diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index df643675357..10e9f6a9488 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -8,6 +8,7 @@ import { parseBoolean } from '../lib/utils/common_utils'; import { resetServiceWorkersPublicPath } from '../lib/utils/webpack'; import ide from './components/ide.vue'; import { createRouter } from './ide_router'; +import { initGitlabWebIDE } from './init_gitlab_web_ide'; import { DEFAULT_THEME } from './lib/themes'; import { createStore } from './stores'; @@ -34,7 +35,7 @@ Vue.use(PerformancePlugin, { * @param {extendStoreCallback} options.extendStore - * Function that receives the default store and returns an extended one. */ -export const initIde = (el, options = {}) => { +export const initLegacyWebIDE = (el, options = {}) => { if (!el) return null; const { rootComponent = ide, extendStore = identity } = options; @@ -93,8 +94,15 @@ export const initIde = (el, options = {}) => { */ export function startIde(options) { const ideElement = document.getElementById('ide'); - if (ideElement) { + + if (!ideElement) { + return; + } + + if (gon.features?.vscodeWebIde) { + initGitlabWebIDE(ideElement); + } else { resetServiceWorkersPublicPath(); - initIde(ideElement, options); + initLegacyWebIDE(ideElement, options); } } diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js new file mode 100644 index 00000000000..a061da38d4f --- /dev/null +++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js @@ -0,0 +1,30 @@ +import { cleanTrailingSlash } from './stores/utils'; + +export const initGitlabWebIDE = async (el) => { + const { start } = await import('@gitlab/web-ide'); + + const { gitlab_url: gitlabUrl } = window.gon; + const baseUrl = new URL(process.env.GITLAB_WEB_IDE_PUBLIC_PATH, window.location.origin); + + // what: Pull what we need from the element. We will replace it soon. + const { path_with_namespace: projectPath } = JSON.parse(el.dataset.project); + const { cspNonce: nonce, branchName: ref } = el.dataset; + + // what: Clean up the element, but preserve id. + // why: This way we don't inherit any `ide-loading` side-effects. This + // mirrors the behavior of Vue when it mounts to an element. + const newEl = document.createElement(el.tagName); + newEl.id = el.id; + newEl.classList.add('gl--flex-center', 'gl-relative', 'gl-h-full'); + + el.replaceWith(newEl); + + // what: Trigger start on our new mounting element + await start(newEl, { + baseUrl: cleanTrailingSlash(baseUrl.href), + projectPath, + gitlabUrl, + ref, + nonce, + }); +}; diff --git a/app/assets/javascripts/pipeline_wizard/components/editor.vue b/app/assets/javascripts/pipeline_wizard/components/editor.vue index 41611233f71..0c063241173 100644 --- a/app/assets/javascripts/pipeline_wizard/components/editor.vue +++ b/app/assets/javascripts/pipeline_wizard/components/editor.vue @@ -27,7 +27,7 @@ export default { data() { return { editor: null, - isUpdating: false, + isFocused: false, yamlEditorExtension: null, }; }, @@ -60,19 +60,23 @@ export default { this.editor.onDidChangeModelContent( debounce(() => this.handleChange(), CONTENT_UPDATE_DEBOUNCE), ); + this.editor.onDidFocusEditorText(() => { + this.isFocused = true; + }); + this.editor.onDidBlurEditorText(() => { + this.isFocused = false; + }); this.updateEditorContent(); this.emitValue(); }, methods: { async updateEditorContent() { - this.isUpdating = true; this.editor.setDoc(this.doc); - this.isUpdating = false; this.requestHighlight(this.highlight); }, handleChange() { this.emitValue(); - if (!this.isUpdating) { + if (this.isFocused) { this.handleTouch(); } }, diff --git a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue index 0fe87bcee7b..1c6b1a167a1 100644 --- a/app/assets/javascripts/pipeline_wizard/components/wrapper.vue +++ b/app/assets/javascripts/pipeline_wizard/components/wrapper.vue @@ -5,6 +5,7 @@ import { uniqueId } from 'lodash'; import { merge } from '~/lib/utils/yaml'; import { __ } from '~/locale'; import { isValidStepSeq } from '~/pipeline_wizard/validators'; +import Tracking from '~/tracking'; import YamlEditor from './editor.vue'; import WizardStep from './step.vue'; import CommitStep from './commit.vue'; @@ -16,6 +17,8 @@ export const i18n = { YAML-file for you to add to your repository`), }; +const trackingMixin = Tracking.mixin(); + export default { name: 'PipelineWizardWrapper', i18n, @@ -25,6 +28,7 @@ export default { WizardStep, CommitStep, }, + mixins: [trackingMixin], props: { steps: { type: Object, @@ -43,6 +47,11 @@ export default { type: String, required: true, }, + templateId: { + type: String, + required: false, + default: null, + }, }, data() { return { @@ -77,6 +86,11 @@ export default { template: this.steps.get(i).get('template', true), })); }, + tracking() { + return { + category: `pipeline_wizard:${this.templateId}`, + }; + }, }, watch: { isLastStep(value) { @@ -84,9 +98,6 @@ export default { }, }, methods: { - getStep(index) { - return this.steps.get(index); - }, resetHighlight() { this.highlightPath = null; }, @@ -106,6 +117,43 @@ export default { }); return doc; }, + onBack() { + this.currentStepIndex -= 1; + this.track('click_button', { + property: 'back', + label: 'pipeline_wizard_navigation', + extras: { + fromStep: this.currentStepIndex + 1, + toStep: this.currentStepIndex, + }, + }); + }, + onNext() { + this.currentStepIndex += 1; + this.track('click_button', { + property: 'next', + label: 'pipeline_wizard_navigation', + extras: { + fromStep: this.currentStepIndex - 1, + toStep: this.currentStepIndex, + }, + }); + }, + onDone() { + this.$emit('done'); + this.track('click_button', { + label: 'pipeline_wizard_commit', + property: 'commit', + }); + }, + onEditorTouched() { + this.track('edit', { + label: 'pipeline_wizard_editor_interaction', + extras: { + currentStep: this.currentStepIndex, + }, + }); + }, }, }; @@ -127,8 +175,8 @@ export default { :file-content="pipelineBlob" :filename="filename" :project-path="projectPath" - @back="currentStepIndex--" - @done="$emit('done')" + @back="onBack" + @done="onDone" /> @@ -162,6 +210,7 @@ export default { :highlight="highlightPath" class="gl-w-full" @update:yaml="onEditorUpdate" + @touch.once="onEditorTouched" />
@@ -60,6 +63,7 @@ export default { :filename="filename" :project-path="projectPath" :steps="steps" + :template-id="templateId" @done="$emit('done')" />
diff --git a/app/assets/javascripts/pipeline_wizard/templates/pages.yml b/app/assets/javascripts/pipeline_wizard/templates/pages.yml index cd2242b1ba7..9d7936f2f5a 100644 --- a/app/assets/javascripts/pipeline_wizard/templates/pages.yml +++ b/app/assets/javascripts/pipeline_wizard/templates/pages.yml @@ -1,3 +1,4 @@ +id: gitlab/pages title: Get started with Pages description: "GitLab Pages lets you deploy static websites in minutes. All you need is a .gitlab-ci.yml file. Follow the below steps to diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb index 9fcb8385312..58a985cbc46 100644 --- a/app/controllers/ide_controller.rb +++ b/app/controllers/ide_controller.rb @@ -13,6 +13,7 @@ class IdeController < ApplicationController push_frontend_feature_flag(:build_service_proxy) push_frontend_feature_flag(:schema_linting) push_frontend_feature_flag(:reject_unsigned_commits_by_gitlab) + push_frontend_feature_flag(:vscode_web_ide, current_user) define_index_vars end diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb index 4b463b9971d..ec1327cf7ae 100644 --- a/app/helpers/ide_helper.rb +++ b/app/helpers/ide_helper.rb @@ -24,7 +24,8 @@ module IdeHelper 'web-terminal-svg-path' => image_path('illustrations/web-ide_promotion.svg'), 'web-terminal-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'interactive-web-terminals-for-the-web-ide'), 'web-terminal-config-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'web-ide-configuration-file'), - 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration') + 'web-terminal-runners-help-path' => help_page_path('user/project/web_ide/index.md', anchor: 'runner-configuration'), + 'csp-nonce' => content_security_policy_nonce } end diff --git a/config/feature_flags/development/vscode_web_ide.yml b/config/feature_flags/development/vscode_web_ide.yml new file mode 100644 index 00000000000..3d29ae40e7c --- /dev/null +++ b/config/feature_flags/development/vscode_web_ide.yml @@ -0,0 +1,8 @@ +--- +name: vscode_web_ide +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95169 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/371084 +milestone: '15.4' +type: development +group: group::editor +default_enabled: false diff --git a/config/webpack.config.js b/config/webpack.config.js index 545262bcb70..d7e6b9089ad 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -6,6 +6,7 @@ const path = require('path'); const BABEL_VERSION = require('@babel/core/package.json').version; const SOURCEGRAPH_VERSION = require('@sourcegraph/code-host-integration/package.json').version; +const GITLAB_WEB_IDE_VERSION = require('@gitlab/web-ide/package.json').version; const BABEL_LOADER_VERSION = require('babel-loader/package.json').version; const CompressionPlugin = require('compression-webpack-plugin'); @@ -60,11 +61,16 @@ const NO_SOURCEMAPS = process.env.NO_SOURCEMAPS && process.env.NO_SOURCEMAPS !== const WEBPACK_OUTPUT_PATH = path.join(ROOT_PATH, 'public/assets/webpack'); const WEBPACK_PUBLIC_PATH = '/assets/webpack/'; const SOURCEGRAPH_PACKAGE = '@sourcegraph/code-host-integration'; +const GITLAB_WEB_IDE_PACKAGE = '@gitlab/web-ide'; const SOURCEGRAPH_PATH = path.join('sourcegraph', SOURCEGRAPH_VERSION, '/'); const SOURCEGRAPH_OUTPUT_PATH = path.join(WEBPACK_OUTPUT_PATH, SOURCEGRAPH_PATH); const SOURCEGRAPH_PUBLIC_PATH = path.join(WEBPACK_PUBLIC_PATH, SOURCEGRAPH_PATH); +const GITLAB_WEB_IDE_PATH = path.join('gitlab-vscode', GITLAB_WEB_IDE_VERSION, '/'); +const GITLAB_WEB_IDE_OUTPUT_PATH = path.join(WEBPACK_OUTPUT_PATH, GITLAB_WEB_IDE_PATH); +const GITLAB_WEB_IDE_PUBLIC_PATH = path.join(WEBPACK_PUBLIC_PATH, GITLAB_WEB_IDE_PATH); + const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map'; let autoEntriesCount = 0; @@ -613,6 +619,10 @@ module.exports = { ignore: ['package.json'], }, }, + { + from: path.join(ROOT_PATH, 'node_modules', GITLAB_WEB_IDE_PACKAGE, 'dist', 'public'), + to: GITLAB_WEB_IDE_OUTPUT_PATH, + }, { from: path.join( ROOT_PATH, @@ -720,6 +730,7 @@ module.exports = { IS_JH: IS_JH ? 'window.gon && window.gon.jh' : JSON.stringify(false), // This is used by Sourcegraph because these assets are loaded dnamically 'process.env.SOURCEGRAPH_PUBLIC_PATH': JSON.stringify(SOURCEGRAPH_PUBLIC_PATH), + 'process.env.GITLAB_WEB_IDE_PUBLIC_PATH': JSON.stringify(GITLAB_WEB_IDE_PUBLIC_PATH), ...(IS_PRODUCTION ? {} : { LIVE_RELOAD: DEV_SERVER_LIVERELOAD }), }), diff --git a/doc/development/cicd/pipeline_wizard.md b/doc/development/cicd/pipeline_wizard.md index 7a0b70bd8e8..227a49d85db 100644 --- a/doc/development/cicd/pipeline_wizard.md +++ b/doc/development/cicd/pipeline_wizard.md @@ -58,7 +58,7 @@ consists of 2-3 steps, for a total of 3-4 steps visible to the user. ```yaml # ~/pipeline_wizard/templates/my_template.yml - +id: gitlab/my-template title: Set up my specific tech pipeline description: Here's two or three introductory sentences that help the user understand what this wizard is going to set up. steps: @@ -156,12 +156,13 @@ Webpack does not parse it as an Object. In the root element of the template file, you can define the following properties: -| Name | Required | Type | Description | -|---------------|------------------------|--------|---------------------------------------------------------------------------------------| -| `title` | **{check-circle}** Yes | string | The page title as displayed to the user. It becomes an `h1` heading above the wizard. | -| `description` | **{check-circle}** Yes | string | The page description as displayed to the user. | -| `filename` | **{dotted-circle}** No | string | The name of the file that is being generated. Defaults to `.gitlab-ci.yml`. | -| `steps` | **{check-circle}** Yes | list | A list of [step definitions](#step-reference). | +| Name | Required | Type | Description | +|---------------|------------------------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | **{check-circle}** Yes | string | A unique template ID. This ID should follow a namespacing pattern, with a forward slash `/` as separator. Templates committed to GitLab source code should always begin with `gitlab`. For example: `gitlab/my-template` | +| `title` | **{check-circle}** Yes | string | The page title as displayed to the user. It becomes an `h1` heading above the wizard. | +| `description` | **{check-circle}** Yes | string | The page description as displayed to the user. | +| `filename` | **{dotted-circle}** No | string | The name of the file that is being generated. Defaults to `.gitlab-ci.yml`. | +| `steps` | **{check-circle}** Yes | list | A list of [step definitions](#step-reference). | ### `step` Reference diff --git a/doc/development/code_review.md b/doc/development/code_review.md index 49235bef2fe..ec3e8220845 100644 --- a/doc/development/code_review.md +++ b/doc/development/code_review.md @@ -326,15 +326,23 @@ Because a maintainer's job only depends on their knowledge of the overall GitLab codebase, and not that of any specific domain, they can review, approve, and merge merge requests from any team and in any product area. +Maintainers are the DRI of assuring that the acceptance criteria of a merge request are reasonably met. +In general, [quality is everyone’s responsibility](https://about.gitlab.com/handbook/engineering/quality/), +but maintainers of an MR are held responsible for **ensuring** that an MR meets those general quality standards. + +If a maintainer feels that an MR is substantial enough, or requires a [domain expert](#domain-experts), +maintainers have the discretion to request a review from another reviewer, or maintainer. Here are some +examples of maintainers proactively doing this during review: + +- +- +- + Maintainers do their best to also review the specifics of the chosen solution before merging, but as they are not necessarily [domain experts](#domain-experts), they may be poorly placed to do so without an unreasonable investment of time. In those cases, they defer to the judgment of the author and earlier reviewers, in favor of focusing on their primary responsibilities. -If a maintainer feels that an MR is substantial enough that it warrants a review from a [domain expert](#domain-experts), -and it is unclear whether a domain expert have been involved in the reviews to date, -they may request a [domain expert's](#domain-experts) review before merging the MR. - If a developer who happens to also be a maintainer was involved in a merge request as a reviewer, it is recommended that they are not also picked as the maintainer to ultimately approve and merge it. diff --git a/package.json b/package.json index 3d3b4c9302f..91ccbab5f08 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@gitlab/svgs": "3.1.0", "@gitlab/ui": "43.7.1", "@gitlab/visual-review-tools": "1.7.3", + "@gitlab/web-ide": "0.0.1-dev-20220815034418", "@rails/actioncable": "6.1.4-7", "@rails/ujs": "6.1.4-7", "@sentry/browser": "5.30.0", diff --git a/spec/features/ide/user_commits_changes_spec.rb b/spec/features/ide/user_commits_changes_spec.rb index e1e586a4f18..04b215710b3 100644 --- a/spec/features/ide/user_commits_changes_spec.rb +++ b/spec/features/ide/user_commits_changes_spec.rb @@ -9,6 +9,8 @@ RSpec.describe 'IDE user commits changes', :js do let(:user) { project.first_owner } before do + stub_feature_flags(vscode_web_ide: false) + sign_in(user) ide_visit(project) diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb index 8f4668d49ee..8a95d7c5544 100644 --- a/spec/features/ide/user_opens_merge_request_spec.rb +++ b/spec/features/ide/user_opens_merge_request_spec.rb @@ -8,6 +8,8 @@ RSpec.describe 'IDE merge request', :js do let(:user) { project.first_owner } before do + stub_feature_flags(vscode_web_ide: false) + sign_in(user) visit(merge_request_path(merge_request)) diff --git a/spec/features/ide_spec.rb b/spec/features/ide_spec.rb index 2505ab0afee..c7c740c2293 100644 --- a/spec/features/ide_spec.rb +++ b/spec/features/ide_spec.rb @@ -4,12 +4,14 @@ require 'spec_helper' RSpec.describe 'IDE', :js do describe 'sub-groups' do + let(:ide_iframe_selector) { '#ide iframe' } let(:user) { create(:user) } let(:group) { create(:group) } let(:subgroup) { create(:group, parent: group) } let(:subgroup_project) { create(:project, :repository, namespace: subgroup) } before do + stub_feature_flags(vscode_web_ide: vscode_ff) subgroup_project.add_maintainer(user) sign_in(user) @@ -20,8 +22,28 @@ RSpec.describe 'IDE', :js do wait_for_requests end - it 'loads project in web IDE' do - expect(page).to have_selector('.context-header', text: subgroup_project.name) + context 'with vscode feature flag on' do + let(:vscode_ff) { true } + + it 'loads project in Web IDE' do + iframe = find(ide_iframe_selector) + + page.within_frame(iframe) do + expect(page).to have_selector('.title', text: subgroup_project.name.upcase) + end + end + end + + context 'with vscode feature flag off' do + let(:vscode_ff) { false } + + it 'loads project in legacy Web IDE' do + expect(page).to have_selector('.context-header', text: subgroup_project.name) + end + + it 'does not load new Web IDE' do + expect(page).not_to have_selector(ide_iframe_selector) + end end end end diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb index 484f740faee..d2774aa74c9 100644 --- a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb +++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb @@ -8,6 +8,10 @@ RSpec.describe 'User creates new blob', :js do let(:user) { create(:user) } let(:project) { create(:project, :empty_repo) } + before do + stub_feature_flags(vscode_web_ide: false) + end + shared_examples 'creating a file' do it 'allows the user to add a new file in Web IDE' do visit project_path(project) diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 0ad44f31a52..52686469243 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -9,6 +9,8 @@ RSpec.describe 'Projects > Files > Project owner sees a link to create a license let(:project_maintainer) { project.first_owner } before do + stub_feature_flags(vscode_web_ide: false) + sign_in(project_maintainer) end diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index ad96f6b6239..1a9c5483218 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -14,6 +14,8 @@ RSpec.describe 'Projects > Files > User edits files', :js do let(:user) { create(:user) } before do + stub_feature_flags(vscode_web_ide: false) + sign_in(user) end diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index 074469a9b55..9c950cfee6e 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -7,6 +7,8 @@ RSpec.describe 'Multi-file editor new directory', :js do let(:project) { create(:project, :repository) } before do + stub_feature_flags(vscode_web_ide: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index 85c644fa528..c0567ed4580 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -7,6 +7,8 @@ RSpec.describe 'Multi-file editor new file', :js do let(:project) { create(:project, :repository) } before do + stub_feature_flags(vscode_web_ide: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb index 163e347d03d..7e971e09455 100644 --- a/spec/features/projects/tree/tree_show_spec.rb +++ b/spec/features/projects/tree/tree_show_spec.rb @@ -117,6 +117,10 @@ RSpec.describe 'Projects tree', :js do end context 'web IDE' do + before do + stub_feature_flags(vscode_web_ide: false) + end + it 'opens folder in IDE' do visit project_tree_path(project, File.join('master', 'bar')) diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb index ce00483bc91..f32141d6051 100644 --- a/spec/features/projects/tree/upload_file_spec.rb +++ b/spec/features/projects/tree/upload_file_spec.rb @@ -9,6 +9,8 @@ RSpec.describe 'Multi-file editor upload file', :js do let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') } before do + stub_feature_flags(vscode_web_ide: false) + project.add_maintainer(user) sign_in(user) diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js new file mode 100644 index 00000000000..ec8559f1b56 --- /dev/null +++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js @@ -0,0 +1,62 @@ +import { start } from '@gitlab/web-ide'; +import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide'; +import { TEST_HOST } from 'helpers/test_constants'; + +jest.mock('@gitlab/web-ide'); + +const ROOT_ELEMENT_ID = 'ide'; +const TEST_NONCE = 'test123nonce'; +const TEST_PROJECT = { path_with_namespace: 'group1/project1' }; +const TEST_BRANCH_NAME = '12345-foo-patch'; +const TEST_GITLAB_URL = 'https://test-gitlab/'; +const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path'; + +describe('ide/init_gitlab_web_ide', () => { + const createRootElement = () => { + const el = document.createElement('div'); + + el.id = ROOT_ELEMENT_ID; + // why: We'll test that this class is removed later + el.classList.add('ide-loading'); + el.dataset.project = JSON.stringify(TEST_PROJECT); + el.dataset.cspNonce = TEST_NONCE; + el.dataset.branchName = TEST_BRANCH_NAME; + + document.body.append(el); + }; + const findRootElement = () => document.getElementById(ROOT_ELEMENT_ID); + const act = () => initGitlabWebIDE(findRootElement()); + + beforeEach(() => { + process.env.GITLAB_WEB_IDE_PUBLIC_PATH = TEST_GITLAB_WEB_IDE_PUBLIC_PATH; + window.gon.gitlab_url = TEST_GITLAB_URL; + + createRootElement(); + + act(); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('calls start with element', () => { + expect(start).toHaveBeenCalledWith(findRootElement(), { + baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`, + projectPath: TEST_PROJECT.path_with_namespace, + ref: TEST_BRANCH_NAME, + gitlabUrl: TEST_GITLAB_URL, + nonce: TEST_NONCE, + }); + }); + + it('clears classes and data from root element', () => { + const rootEl = findRootElement(); + + // why: Snapshot to test that `ide-loading` was removed and no other + // artifacts are remaining. + expect(rootEl.outerHTML).toBe( + '
', + ); + }); +}); diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js index 540a08d2c7f..b4ddc6e7d42 100644 --- a/spec/frontend/pipeline_wizard/components/editor_spec.js +++ b/spec/frontend/pipeline_wizard/components/editor_spec.js @@ -57,13 +57,4 @@ describe('Pages Yaml Editor wrapper', () => { }); }); }); - - describe('events', () => { - const wrapper = mount(YamlEditor, defaultOptions); - - it('emits touch if content is changed in editor', async () => { - await wrapper.vm.editor.setValue('foo: boo'); - expect(wrapper.emitted('touch')).toEqual([expect.any(Array)]); - }); - }); }); diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js index 357a9d21723..9e73ca6c9b7 100644 --- a/spec/frontend/pipeline_wizard/components/wrapper_spec.js +++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js @@ -2,6 +2,7 @@ import { Document, parseDocument } from 'yaml'; import { GlProgressBar } from '@gitlab/ui'; import { nextTick } from 'vue'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; +import { mockTracking } from 'helpers/tracking_helper'; import PipelineWizardWrapper, { i18n } from '~/pipeline_wizard/components/wrapper.vue'; import WizardStep from '~/pipeline_wizard/components/step.vue'; import CommitStep from '~/pipeline_wizard/components/commit.vue'; @@ -19,9 +20,11 @@ describe('Pipeline Wizard - wrapper.vue', () => { const steps = parseDocument(stepsYaml).toJS(); const getAsYamlNode = (value) => new Document(value).contents; + const templateId = 'my-namespace/my-template'; const createComponent = (props = {}, mountFn = shallowMountExtended) => { wrapper = mountFn(PipelineWizardWrapper, { propsData: { + templateId, projectPath: '/user/repo', defaultBranch: 'main', filename: '.gitlab-ci.yml', @@ -311,4 +314,126 @@ describe('Pipeline Wizard - wrapper.vue', () => { }); }); }); + + describe('when commit step done', () => { + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('emits done', () => { + expect(wrapper.emitted('done')).toBeUndefined(); + + wrapper.findComponent(CommitStep).vm.$emit('done'); + + expect(wrapper.emitted('done')).toHaveLength(1); + }); + }); + + describe('tracking', () => { + let trackingSpy; + const trackingCategory = `pipeline_wizard:${templateId}`; + + const setUpTrackingSpy = () => { + trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn); + }; + + it('tracks next button click event', () => { + createComponent(); + setUpTrackingSpy(); + findFirstVisibleStep().vm.$emit('next'); + + expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', { + category: trackingCategory, + property: 'next', + label: 'pipeline_wizard_navigation', + extras: { + fromStep: 0, + toStep: 1, + }, + }); + }); + + it('tracks back button click event', () => { + createComponent(); + + // Navigate to step 1 without the spy set up + findFirstVisibleStep().vm.$emit('next'); + + // Now enable the tracking spy + setUpTrackingSpy(); + + findFirstVisibleStep().vm.$emit('back'); + + expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', { + category: trackingCategory, + property: 'back', + label: 'pipeline_wizard_navigation', + extras: { + fromStep: 1, + toStep: 0, + }, + }); + }); + + it('tracks back button click event on the commit step', () => { + createComponent(); + + // Navigate to step 2 without the spy set up + findFirstVisibleStep().vm.$emit('next'); + findFirstVisibleStep().vm.$emit('next'); + + // Now enable the tracking spy + setUpTrackingSpy(); + + wrapper.findComponent(CommitStep).vm.$emit('back'); + + expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', { + category: trackingCategory, + property: 'back', + label: 'pipeline_wizard_navigation', + extras: { + fromStep: 2, + toStep: 1, + }, + }); + }); + + it('tracks done event on the commit step', () => { + createComponent(); + + // Navigate to step 2 without the spy set up + findFirstVisibleStep().vm.$emit('next'); + findFirstVisibleStep().vm.$emit('next'); + + // Now enable the tracking spy + setUpTrackingSpy(); + + wrapper.findComponent(CommitStep).vm.$emit('done'); + + expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'click_button', { + category: trackingCategory, + label: 'pipeline_wizard_commit', + property: 'commit', + }); + }); + + it('tracks when editor emits touch events', () => { + createComponent(); + setUpTrackingSpy(); + + wrapper.findComponent(YamlEditor).vm.$emit('touch'); + + expect(trackingSpy).toHaveBeenCalledWith(trackingCategory, 'edit', { + category: trackingCategory, + label: 'pipeline_wizard_editor_interaction', + extras: { + currentStep: 0, + }, + }); + }); + }); }); diff --git a/spec/frontend/pipeline_wizard/mock/yaml.js b/spec/frontend/pipeline_wizard/mock/yaml.js index e7087b59ce7..12b6f1052b2 100644 --- a/spec/frontend/pipeline_wizard/mock/yaml.js +++ b/spec/frontend/pipeline_wizard/mock/yaml.js @@ -71,6 +71,7 @@ bar: barVal `; export const fullTemplate = ` +id: test/full-template title: some title description: some description filename: foo.yml @@ -84,6 +85,7 @@ steps: `; export const fullTemplateWithoutFilename = ` +id: test/full-template-no-filename title: some title description: some description steps: diff --git a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js index 3f689ffdbc8..13234525159 100644 --- a/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js +++ b/spec/frontend/pipeline_wizard/pipeline_wizard_spec.js @@ -59,6 +59,7 @@ describe('PipelineWizard', () => { defaultBranch, projectPath, filename: parseDocument(template).get('filename'), + templateId: parseDocument(template).get('id'), }), ); }); diff --git a/spec/frontend_integration/ide/helpers/start.js b/spec/frontend_integration/ide/helpers/start.js index 925db12f36e..40e6a725358 100644 --- a/spec/frontend_integration/ide/helpers/start.js +++ b/spec/frontend_integration/ide/helpers/start.js @@ -1,7 +1,7 @@ import { editor as monacoEditor } from 'monaco-editor'; import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; -import { initIde } from '~/ide'; +import { initLegacyWebIDE } from '~/ide'; import extendStore from '~/ide/stores/extend'; import { getProject, getEmptyProject } from 'jest/../frontend_integration/test_helpers/fixtures'; import { IDE_DATASET } from './mock_data'; @@ -16,7 +16,7 @@ export default (container, { isRepoEmpty = false, path = '', mrId = '' } = {}) = const el = document.createElement('div'); Object.assign(el.dataset, IDE_DATASET, { project: JSON.stringify(project) }); container.appendChild(el); - const vm = initIde(el, { extendStore }); + const vm = initLegacyWebIDE(el, { extendStore }); // We need to dispose of editor Singleton things or tests will bump into eachother vm.$on('destroy', () => monacoEditor.getModels().forEach((model) => model.dispose())); diff --git a/yarn.lock b/yarn.lock index 1247e8932a5..615031ba714 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1075,6 +1075,13 @@ resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235" integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g== +"@gitlab/web-ide@0.0.1-dev-20220815034418": + version "0.0.1-dev-20220815034418" + resolved "https://registry.yarnpkg.com/@gitlab/web-ide/-/web-ide-0.0.1-dev-20220815034418.tgz#c4c4f72d6ffe1ba18fdfc452b30d10ff7cde0550" + integrity sha512-cYTMAXfc+B549euU+IDHgFK0Rjwa5SNjs4coBJOD4eoZVvPd3YtHbArqqzmdO71SYjCqV1Bl4v0Jy4UnOJgcjQ== + dependencies: + mustache "^4.2.0" + "@graphql-eslint/eslint-plugin@3.10.7": version "3.10.7" resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.10.7.tgz#9a203e2084371eca933d88b73ce7a6bebbbb9872" @@ -8905,6 +8912,11 @@ multicast-dns@^7.2.4: dns-packet "^5.2.2" thunky "^1.0.2" +mustache@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64" + integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ== + nanoid@^3.3.4: version "3.3.4" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"