diff --git a/.eslint_todo/vue-no-unused-properties.mjs b/.eslint_todo/vue-no-unused-properties.mjs index bcf862aaed7..7b35980e9fe 100644 --- a/.eslint_todo/vue-no-unused-properties.mjs +++ b/.eslint_todo/vue-no-unused-properties.mjs @@ -95,23 +95,6 @@ export default { 'app/assets/javascripts/google_cloud/gcp_regions/list.vue', 'app/assets/javascripts/groups/components/group_item.vue', 'app/assets/javascripts/groups/components/invite_members_banner.vue', - 'app/assets/javascripts/ide/components/commit_sidebar/form.vue', - 'app/assets/javascripts/ide/components/commit_sidebar/list_item.vue', - 'app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue', - 'app/assets/javascripts/ide/components/file_row_extra.vue', - 'app/assets/javascripts/ide/components/ide.vue', - 'app/assets/javascripts/ide/components/ide_side_bar.vue', - 'app/assets/javascripts/ide/components/ide_status_bar.vue', - 'app/assets/javascripts/ide/components/ide_tree.vue', - 'app/assets/javascripts/ide/components/ide_tree_list.vue', - 'app/assets/javascripts/ide/components/jobs/item.vue', - 'app/assets/javascripts/ide/components/merge_requests/list.vue', - 'app/assets/javascripts/ide/components/new_dropdown/modal.vue', - 'app/assets/javascripts/ide/components/panes/right.vue', - 'app/assets/javascripts/ide/components/pipelines/list.vue', - 'app/assets/javascripts/ide/components/repo_commit_section.vue', - 'app/assets/javascripts/ide/components/repo_editor.vue', - 'app/assets/javascripts/ide/components/repo_tabs.vue', 'app/assets/javascripts/import_entities/import_groups/components/import_actions_cell.vue', 'app/assets/javascripts/import_entities/import_groups/components/import_table.vue', 'app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue', diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS index 9a179a054ac..591756a836e 100644 --- a/.gitlab/CODEOWNERS +++ b/.gitlab/CODEOWNERS @@ -1251,6 +1251,7 @@ lib/gitlab/checks/** # i18n Docs [Localization Team] @gitlab-com/localization/maintainers /doc-locale/** +/doc/development/i18n/proofreader.md [Authorization] @gitlab-org/software-supply-chain-security/authorization/approvers /config/initializers/declarative_policy.rb diff --git a/.stylelintrc b/.stylelintrc index 12556a59534..10a80ad593e 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -21,8 +21,7 @@ { "files": [ "app/assets/stylesheets/application_dark.scss", - "app/assets/stylesheets/framework/**/*.scss", - "app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss" + "app/assets/stylesheets/framework/**/*.scss" ], "rules": { "gitlab/no-gl-class": null diff --git a/GITLAB_KAS_VERSION b/GITLAB_KAS_VERSION index 49897710c48..18c54862ee4 100644 --- a/GITLAB_KAS_VERSION +++ b/GITLAB_KAS_VERSION @@ -1 +1 @@ -c9a38829a0c35e0d325941304873f715fb10507e +b8a5f8c7440eb511a10903d7f3d1862c96cd6428 diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 591f27924c5..19ba2dc71ea 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -163,7 +163,7 @@ export default { :title="restProjectsTooltip" class="deploy-project-label gl-mb-2 gl-mr-2 gl-truncate" href="#" - @click.native="toggleExpanded" + @click="toggleExpanded" > {{ restProjectsLabel }} diff --git a/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js b/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js deleted file mode 100644 index 6270517b3f3..00000000000 --- a/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * A WebIDE Extension options for Source Editor - * @typedef {Object} WebIDEExtensionOptions - * @property {Object} modelManager The root manager for WebIDE models - * @property {Object} store The state store for communication - * @property {Object} file - * @property {Object} options The Monaco editor options - */ - -import { KeyCode, KeyMod, Range } from 'monaco-editor'; -import { EDITOR_TYPE_DIFF } from '~/editor/constants'; -import Disposable from '~/ide/lib/common/disposable'; -import { editorOptions } from '~/ide/lib/editor_options'; -import keymap from '~/ide/lib/keymap.json'; - -const isDiffEditorType = (instance) => { - return instance.getEditorType() === EDITOR_TYPE_DIFF; -}; - -export const UPDATE_DIMENSIONS_DELAY = 200; -const defaultOptions = { - modelManager: undefined, - store: undefined, - file: undefined, - options: {}, -}; - -const addActions = (instance, store) => { - const getKeyCode = (key) => { - const monacoKeyMod = key.indexOf('KEY_') === 0; - - return monacoKeyMod ? KeyCode[key] : KeyMod[key]; - }; - - keymap.forEach((command) => { - const { bindings, id, label, action } = command; - - const keybindings = bindings.map((binding) => { - const keys = binding.split('+'); - - // eslint-disable-next-line no-bitwise - return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]); - }); - - instance.addAction({ - id, - label, - keybindings, - run() { - store.dispatch(action.name, action.params); - return null; - }, - }); - }); -}; - -const renderSideBySide = (domElement) => { - return domElement.offsetWidth >= 700; -}; - -const updateDiffInstanceRendering = (instance) => { - instance.updateOptions({ - renderSideBySide: renderSideBySide(instance.getDomNode()), - }); -}; - -export class EditorWebIdeExtension { - static get extensionName() { - return 'EditorWebIde'; - } - - /** - * Set up the WebIDE extension for Source Editor - * @param {module:source_editor_instance~EditorInstance} instance - The Source Editor instance - * @param {WebIDEExtensionOptions} setupOptions - */ - onSetup(instance, setupOptions = defaultOptions) { - this.modelManager = setupOptions.modelManager; - this.store = setupOptions.store; - this.file = setupOptions.file; - this.options = setupOptions.options; - - this.disposable = new Disposable(); - addActions(instance, setupOptions.store); - - if (isDiffEditorType(instance)) { - updateDiffInstanceRendering(instance); - instance.getModifiedEditor().onDidLayoutChange(() => { - updateDiffInstanceRendering(instance); - }); - } - - instance.onDidDispose(() => { - this.onUnuse(); - }); - } - - onUnuse() { - // catch any potential errors with disposing the error - // this is mainly for tests caused by elements not existing - try { - this.disposable.dispose(); - } catch (e) { - if (process.env.NODE_ENV !== 'test') { - // eslint-disable-next-line no-console - console.error(e); - } - } - } - - provides() { - return { - createModel: (instance, file, head = null) => { - return this.modelManager.addModel(file, head); - }, - attachModel: (instance, model) => { - if (isDiffEditorType(instance)) { - instance.setModel({ - original: model.getOriginalModel(), - modified: model.getModel(), - }); - - return; - } - - instance.setModel(model.getModel()); - - instance.updateOptions( - editorOptions.reduce((acc, obj) => { - Object.keys(obj).forEach((key) => { - Object.assign(acc, { - [key]: obj[key](model), - }); - }); - return acc; - }, {}), - ); - }, - attachMergeRequestModel: (instance, model) => { - instance.setModel({ - original: model.getBaseModel(), - modified: model.getModel(), - }); - }, - setPos: (instance, { lineNumber, column }) => { - instance.revealPositionInCenter({ - lineNumber, - column, - }); - instance.setPosition({ - lineNumber, - column, - }); - }, - onPositionChange: (instance, cb) => { - if (typeof instance.onDidChangeCursorPosition !== 'function') { - return; - } - - this.disposable.add(instance.onDidChangeCursorPosition((e) => cb(instance, e))); - }, - replaceSelectedText: (instance, text) => { - let selection = instance.getSelection(); - const range = new Range( - selection.startLineNumber, - selection.startColumn, - selection.endLineNumber, - selection.endColumn, - ); - - instance.executeEdits('', [{ range, text }]); - - selection = instance.getSelection(); - instance.setPosition({ lineNumber: selection.endLineNumber, column: selection.endColumn }); - }, - }; - } -} diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_connection_status_badge.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_connection_status_badge.vue index a21e72cba3c..b7ae2d6cb96 100644 --- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_connection_status_badge.vue +++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_connection_status_badge.vue @@ -89,7 +89,7 @@ export default { :icon="statusIcon" :href="badgeHref" tabindex="0" - @click.native="onClick" + @click="onClick" > {{ badgeText }} diff --git a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_status_bar.vue b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_status_bar.vue index 82cc9f859f9..1e98fce1a67 100644 --- a/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_status_bar.vue +++ b/app/assets/javascripts/environments/environment_details/components/kubernetes/kubernetes_status_bar.vue @@ -256,7 +256,7 @@ export default { data-testid="sync-badge" tabindex="0" :href="fluxBadgeHref" - @click.native="toggleFluxResource('')" + @click="toggleFluxResource('')" >{{ syncStatusBadge.text }} {{ title }} diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue deleted file mode 100644 index d941637a770..00000000000 --- a/app/assets/javascripts/ide/components/activity_bar.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/branches/item.vue b/app/assets/javascripts/ide/components/branches/item.vue deleted file mode 100644 index 80d6d2f2e18..00000000000 --- a/app/assets/javascripts/ide/components/branches/item.vue +++ /dev/null @@ -1,50 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue deleted file mode 100644 index 8048aaab667..00000000000 --- a/app/assets/javascripts/ide/components/branches/search_list.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/cannot_push_code_alert.vue b/app/assets/javascripts/ide/components/cannot_push_code_alert.vue deleted file mode 100644 index d3e51e6e140..00000000000 --- a/app/assets/javascripts/ide/components/cannot_push_code_alert.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue deleted file mode 100644 index 1a47c71e9f4..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ /dev/null @@ -1,98 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue deleted file mode 100644 index 4886e7d835a..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue b/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue deleted file mode 100644 index 22b5ef6d11f..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/empty_state.vue +++ /dev/null @@ -1,24 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue deleted file mode 100644 index 6eff0e317ea..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue +++ /dev/null @@ -1,230 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue deleted file mode 100644 index b1a66629aec..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ /dev/null @@ -1,129 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue deleted file mode 100644 index d04b1db7134..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue deleted file mode 100644 index 98c4f02bb7d..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue +++ /dev/null @@ -1,129 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue b/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue deleted file mode 100644 index c77180467ea..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/new_merge_request_option.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue deleted file mode 100644 index 23377ccb0cd..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ /dev/null @@ -1,104 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue deleted file mode 100644 index 60b5adf62e3..00000000000 --- a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue deleted file mode 100644 index d40aab8ee4f..00000000000 --- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue deleted file mode 100644 index 0eb781e0ba2..00000000000 --- a/app/assets/javascripts/ide/components/error_message.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue deleted file mode 100644 index 01f1e898438..00000000000 --- a/app/assets/javascripts/ide/components/file_row_extra.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/file_templates/bar.vue b/app/assets/javascripts/ide/components/file_templates/bar.vue deleted file mode 100644 index 75a04ce3750..00000000000 --- a/app/assets/javascripts/ide/components/file_templates/bar.vue +++ /dev/null @@ -1,129 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue deleted file mode 100644 index 18f0aa1bce8..00000000000 --- a/app/assets/javascripts/ide/components/ide.vue +++ /dev/null @@ -1,238 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/ide_file_row.vue b/app/assets/javascripts/ide/components/ide_file_row.vue deleted file mode 100644 index 72d63f6a4ad..00000000000 --- a/app/assets/javascripts/ide/components/ide_file_row.vue +++ /dev/null @@ -1,49 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/ide_project_header.vue b/app/assets/javascripts/ide/components/ide_project_header.vue deleted file mode 100644 index c755544e706..00000000000 --- a/app/assets/javascripts/ide/components/ide_project_header.vue +++ /dev/null @@ -1,34 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/ide_review.vue b/app/assets/javascripts/ide/components/ide_review.vue deleted file mode 100644 index 824589b8f1b..00000000000 --- a/app/assets/javascripts/ide/components/ide_review.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue deleted file mode 100644 index ebed76cc1e3..00000000000 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ /dev/null @@ -1,61 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue deleted file mode 100644 index a1a4ae3c683..00000000000 --- a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue +++ /dev/null @@ -1,83 +0,0 @@ - - diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue deleted file mode 100644 index 0c464606a98..00000000000 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ /dev/null @@ -1,137 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/ide_status_list.vue b/app/assets/javascripts/ide/components/ide_status_list.vue deleted file mode 100644 index 26e96aedce0..00000000000 --- a/app/assets/javascripts/ide/components/ide_status_list.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/ide_status_mr.vue b/app/assets/javascripts/ide/components/ide_status_mr.vue deleted file mode 100644 index 067545ae5f8..00000000000 --- a/app/assets/javascripts/ide/components/ide_status_mr.vue +++ /dev/null @@ -1,30 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue deleted file mode 100644 index 73325fb6d96..00000000000 --- a/app/assets/javascripts/ide/components/ide_tree.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue deleted file mode 100644 index 0228440bfeb..00000000000 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ /dev/null @@ -1,83 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue deleted file mode 100644 index 6d808294d92..00000000000 --- a/app/assets/javascripts/ide/components/jobs/detail.vue +++ /dev/null @@ -1,121 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue deleted file mode 100644 index 9f749bb8b0c..00000000000 --- a/app/assets/javascripts/ide/components/jobs/detail/description.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue b/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue deleted file mode 100644 index 1128d680d1d..00000000000 --- a/app/assets/javascripts/ide/components/jobs/detail/scroll_button.vue +++ /dev/null @@ -1,65 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/jobs/item.vue b/app/assets/javascripts/ide/components/jobs/item.vue deleted file mode 100644 index dcae6b70d4f..00000000000 --- a/app/assets/javascripts/ide/components/jobs/item.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue deleted file mode 100644 index 9f5da1d1217..00000000000 --- a/app/assets/javascripts/ide/components/jobs/list.vue +++ /dev/null @@ -1,43 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue deleted file mode 100644 index dc539f532a3..00000000000 --- a/app/assets/javascripts/ide/components/jobs/stage.vue +++ /dev/null @@ -1,93 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue deleted file mode 100644 index 012871a8aae..00000000000 --- a/app/assets/javascripts/ide/components/merge_requests/item.vue +++ /dev/null @@ -1,51 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue deleted file mode 100644 index 5bead0d2f1d..00000000000 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue deleted file mode 100644 index 47e80a4b938..00000000000 --- a/app/assets/javascripts/ide/components/mr_file_icon.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/nav_dropdown.vue b/app/assets/javascripts/ide/components/nav_dropdown.vue deleted file mode 100644 index 99ece59cbda..00000000000 --- a/app/assets/javascripts/ide/components/nav_dropdown.vue +++ /dev/null @@ -1,54 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/nav_dropdown_button.vue b/app/assets/javascripts/ide/components/nav_dropdown_button.vue deleted file mode 100644 index a7154e24f05..00000000000 --- a/app/assets/javascripts/ide/components/nav_dropdown_button.vue +++ /dev/null @@ -1,56 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/nav_form.vue b/app/assets/javascripts/ide/components/nav_form.vue deleted file mode 100644 index 750c2c3e215..00000000000 --- a/app/assets/javascripts/ide/components/nav_form.vue +++ /dev/null @@ -1,35 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/new_dropdown/button.vue b/app/assets/javascripts/ide/components/new_dropdown/button.vue deleted file mode 100644 index 36b061df697..00000000000 --- a/app/assets/javascripts/ide/components/new_dropdown/button.vue +++ /dev/null @@ -1,60 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue deleted file mode 100644 index 27adbeed0d3..00000000000 --- a/app/assets/javascripts/ide/components/new_dropdown/index.vue +++ /dev/null @@ -1,118 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue deleted file mode 100644 index 77b99db61eb..00000000000 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ /dev/null @@ -1,196 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue deleted file mode 100644 index ee76c3e9673..00000000000 --- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue +++ /dev/null @@ -1,88 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue deleted file mode 100644 index e76753daaa2..00000000000 --- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue +++ /dev/null @@ -1,101 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue deleted file mode 100644 index d8896e5321d..00000000000 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/pipelines/empty_state.vue b/app/assets/javascripts/ide/components/pipelines/empty_state.vue deleted file mode 100644 index 321c9ce5fff..00000000000 --- a/app/assets/javascripts/ide/components/pipelines/empty_state.vue +++ /dev/null @@ -1,37 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue deleted file mode 100644 index 649dea1e83d..00000000000 --- a/app/assets/javascripts/ide/components/pipelines/list.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue deleted file mode 100644 index 0452d566313..00000000000 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ /dev/null @@ -1,72 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue deleted file mode 100644 index fdf383ccf59..00000000000 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ /dev/null @@ -1,540 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue deleted file mode 100644 index 72c56daf69c..00000000000 --- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue +++ /dev/null @@ -1,33 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue deleted file mode 100644 index 15cb0571cbf..00000000000 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ /dev/null @@ -1,103 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue deleted file mode 100644 index ae8becea242..00000000000 --- a/app/assets/javascripts/ide/components/repo_tabs.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/resizable_panel.vue b/app/assets/javascripts/ide/components/resizable_panel.vue deleted file mode 100644 index 660057f8f98..00000000000 --- a/app/assets/javascripts/ide/components/resizable_panel.vue +++ /dev/null @@ -1,68 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue deleted file mode 100644 index beacaac9876..00000000000 --- a/app/assets/javascripts/ide/components/shared/tokened_input.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/terminal/empty_state.vue b/app/assets/javascripts/ide/components/terminal/empty_state.vue deleted file mode 100644 index f45d87ef68f..00000000000 --- a/app/assets/javascripts/ide/components/terminal/empty_state.vue +++ /dev/null @@ -1,73 +0,0 @@ - - diff --git a/app/assets/javascripts/ide/components/terminal/session.vue b/app/assets/javascripts/ide/components/terminal/session.vue deleted file mode 100644 index 88401f3bddb..00000000000 --- a/app/assets/javascripts/ide/components/terminal/session.vue +++ /dev/null @@ -1,57 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/terminal/terminal.vue b/app/assets/javascripts/ide/components/terminal/terminal.vue deleted file mode 100644 index 223e7162fb4..00000000000 --- a/app/assets/javascripts/ide/components/terminal/terminal.vue +++ /dev/null @@ -1,119 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/terminal/terminal_controls.vue b/app/assets/javascripts/ide/components/terminal/terminal_controls.vue deleted file mode 100644 index 4c13b4ef103..00000000000 --- a/app/assets/javascripts/ide/components/terminal/terminal_controls.vue +++ /dev/null @@ -1,27 +0,0 @@ - - diff --git a/app/assets/javascripts/ide/components/terminal/view.vue b/app/assets/javascripts/ide/components/terminal/view.vue deleted file mode 100644 index 64de777ca55..00000000000 --- a/app/assets/javascripts/ide/components/terminal/view.vue +++ /dev/null @@ -1,42 +0,0 @@ - - - - diff --git a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue deleted file mode 100644 index dfd252880f8..00000000000 --- a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status.vue +++ /dev/null @@ -1,80 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status_safe.vue b/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status_safe.vue deleted file mode 100644 index 214a13a6668..00000000000 --- a/app/assets/javascripts/ide/components/terminal_sync/terminal_sync_status_safe.vue +++ /dev/null @@ -1,23 +0,0 @@ - - - diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index c92fe21068d..484d76fb75d 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -1,35 +1,16 @@ -export const MAX_WINDOW_HEIGHT_COMPACT = 750; +export const DEFAULT_BRANCH = 'main'; -// Commit message textarea -export const MAX_TITLE_LENGTH = 50; -export const MAX_BODY_LENGTH = 72; +export const GITLAB_WEB_IDE_FEEDBACK_ISSUE = 'https://gitlab.com/gitlab-org/gitlab/-/issues/377367'; -export const SIDEBAR_INIT_WIDTH = 340; -export const SIDEBAR_MIN_WIDTH = 260; -export const SIDEBAR_NAV_WIDTH = 60; +export const IDE_ELEMENT_ID = 'ide'; -// File view modes -export const FILE_VIEW_MODE_EDITOR = 'editor'; -export const FILE_VIEW_MODE_PREVIEW = 'preview'; - -export const PERMISSION_CREATE_MR = 'createMergeRequestIn'; -export const PERMISSION_READ_MR = 'readMergeRequest'; -export const PERMISSION_PUSH_CODE = 'pushCode'; -export const PUSH_RULE_REJECT_UNSIGNED_COMMITS = 'rejectUnsignedCommits'; - -// The default permission object to use when the project data isn't available yet. -// This helps us encapsulate checks like `canPushCode` without requiring an -// additional check like `currentProject && canPushCode`. -export const DEFAULT_PERMISSIONS = { - [PERMISSION_PUSH_CODE]: true, -}; - -export const viewerTypes = { - mr: 'mrdiff', - edit: 'editor', - diff: 'diff', -}; +// note: This path comes from `config/routes.rb` +export const IDE_PATH = '/-/ide'; +export const WEB_IDE_OAUTH_CALLBACK_URL_PATH = '/-/ide/oauth_redirect'; +/** + * LEGACY WEB IDE CONSTANTS USED BY OTHER FRONTEND FEATURES. DO NOT CONTINUE USING. + */ export const diffModes = { replaced: 'replaced', new: 'new', @@ -54,24 +35,6 @@ export const diffViewerErrors = Object.freeze({ stored_externally: 'server_side_but_stored_externally', }); -export const leftSidebarViews = { - edit: { name: 'ide-tree' }, - review: { name: 'ide-review' }, - commit: { name: 'repo-commit-section' }, -}; - -export const rightSidebarViews = { - pipelines: { name: 'pipelines-list', keepAlive: true }, - jobsDetail: { name: 'jobs-detail', keepAlive: false }, - mergeRequestInfo: { name: 'merge-request-info', keepAlive: true }, - terminal: { name: 'terminal', keepAlive: true }, -}; - -export const stageKeys = { - unstaged: 'unstaged', - staged: 'staged', -}; - export const commitItemIconMap = { addition: { icon: 'file-addition', @@ -86,33 +49,3 @@ export const commitItemIconMap = { class: 'file-deletion ide-file-deletion', }, }; - -export const modalTypes = { - rename: 'rename', - tree: 'tree', - blob: 'blob', -}; - -export const commitActionTypes = { - move: 'move', - delete: 'delete', - create: 'create', - update: 'update', -}; - -export const SIDE_LEFT = 'left'; -export const SIDE_RIGHT = 'right'; - -// This is the maximum number of files to auto open when opening the Web IDE -// from a merge request -export const MAX_MR_FILES_AUTO_OPEN = 10; - -export const DEFAULT_BRANCH = 'main'; - -export const GITLAB_WEB_IDE_FEEDBACK_ISSUE = 'https://gitlab.com/gitlab-org/gitlab/-/issues/377367'; - -export const IDE_ELEMENT_ID = 'ide'; - -// note: This path comes from `config/routes.rb` -export const IDE_PATH = '/-/ide'; -export const WEB_IDE_OAUTH_CALLBACK_URL_PATH = '/-/ide/oauth_redirect'; diff --git a/app/assets/javascripts/ide/eventhub.js b/app/assets/javascripts/ide/eventhub.js deleted file mode 100644 index e31806ad199..00000000000 --- a/app/assets/javascripts/ide/eventhub.js +++ /dev/null @@ -1,3 +0,0 @@ -import createEventHub from '~/helpers/event_hub_factory'; - -export default createEventHub(); diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js deleted file mode 100644 index 1365a7aa565..00000000000 --- a/app/assets/javascripts/ide/ide_router.js +++ /dev/null @@ -1,112 +0,0 @@ -import Vue from 'vue'; -import IdeRouter from '~/ide/ide_router_extension'; -import { joinPaths } from '~/lib/utils/url_utility'; -import { - WEBIDE_MARK_FETCH_PROJECT_DATA_START, - WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH, - WEBIDE_MEASURE_FETCH_PROJECT_DATA, -} from '~/performance/constants'; -import { performanceMarkAndMeasure } from '~/performance/utils'; -import { syncRouterAndStore } from './sync_router_and_store'; - -Vue.use(IdeRouter); - -/** - * Routes below /-/ide/: - -/project/h5bp/html5-boilerplate/blob/main -/project/h5bp/html5-boilerplate/blob/main/app/js/test.js - -/project/h5bp/html5-boilerplate/mr/123 -/project/h5bp/html5-boilerplate/mr/123/app/js/test.js - -/workspace/123 -/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch -/workspace/project/h5bp/html5-boilerplate/mr/123 - -/ = /workspace - -/settings -*/ - -// Unfortunately Vue Router doesn't work without at least a fake component -// If you do only data handling -export const EmptyRouterComponent = { - render(createElement) { - return createElement('div'); - }, -}; - -export const createRouter = (store, defaultBranch) => { - const router = new IdeRouter({ - mode: 'history', - base: joinPaths(gon.relative_url_root || '', '/-/ide/'), - routes: [ - { - path: '/project/:namespace+/:project', - component: EmptyRouterComponent, - children: [ - { - path: ':targetmode(edit|tree|blob)/:branchid+/-/*', - component: EmptyRouterComponent, - }, - { - path: ':targetmode(edit|tree|blob)/:branchid+/', - redirect: (to) => joinPaths(to.path, '/-/'), - }, - { - path: ':targetmode(edit|tree|blob)', - redirect: (to) => joinPaths(to.path, `/${defaultBranch}/-/`), - }, - { - path: 'merge_requests/:mrid', - component: EmptyRouterComponent, - }, - { - path: '', - redirect: (to) => joinPaths(to.path, `/edit/${defaultBranch}/-/`), - }, - ], - }, - ], - }); - - router.beforeEach((to, from, next) => { - if (to.params.namespace && to.params.project) { - const basePath = to.params.pathMatch || ''; - const projectId = `${to.params.namespace}/${to.params.project}`; - const branchId = to.params.branchid; - const mergeRequestId = to.params.mrid; - - performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_PROJECT_DATA_START }); - if (branchId) { - performanceMarkAndMeasure({ - mark: WEBIDE_MARK_FETCH_PROJECT_DATA_FINISH, - measures: [ - { - name: WEBIDE_MEASURE_FETCH_PROJECT_DATA, - start: WEBIDE_MARK_FETCH_PROJECT_DATA_START, - }, - ], - }); - store.dispatch('openBranch', { - projectId, - branchId, - basePath, - }); - } else if (mergeRequestId) { - store.dispatch('openMergeRequest', { - projectId, - mergeRequestId, - targetProjectId: to.query.target_project, - }); - } - } - - next(); - }); - - syncRouterAndStore(router, store); - - return router; -}; diff --git a/app/assets/javascripts/ide/ide_router_extension.js b/app/assets/javascripts/ide/ide_router_extension.js deleted file mode 100644 index a146aca7283..00000000000 --- a/app/assets/javascripts/ide/ide_router_extension.js +++ /dev/null @@ -1,21 +0,0 @@ -import VueRouter from 'vue-router'; -import { escapeFileUrl } from '~/lib/utils/url_utility'; - -// To allow special characters (like "#," for example) in the branch names, we -// should encode all the locations before those get processed by History API. -// Otherwise, paths get messed up so that the router receives incorrect -// branchid. The only way to do it consistently and in a more or less -// future-proof manner is, unfortunately, to monkey-patch VueRouter or, as -// suggested here, achieve the same more reliably by subclassing VueRouter and -// update the methods, used in WebIDE. -// -// More context: https://gitlab.com/gitlab-org/gitlab/issues/35473 - -export default class IDERouter extends VueRouter { - push(location, onComplete, onAbort) { - super.push(escapeFileUrl(location), onComplete, onAbort); - } - resolve(to, current, append) { - return super.resolve(escapeFileUrl(to), current, append); - } -} diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 5d126a2e000..28f2016a704 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -1,15 +1,10 @@ import Vue from 'vue'; import { IDE_ELEMENT_ID } from '~/ide/constants'; -import PerformancePlugin from '~/performance/vue_performance_plugin'; import Translate from '~/vue_shared/translate'; import { OAuthCallbackDomainMismatchErrorApp } from './oauth_callback_domain_mismatch_error'; Vue.use(Translate); -Vue.use(PerformancePlugin, { - components: ['FileTree'], -}); - /** * Start the IDE. * diff --git a/app/assets/javascripts/ide/init_legacy_web_ide.js b/app/assets/javascripts/ide/init_legacy_web_ide.js deleted file mode 100644 index 5cd993ae747..00000000000 --- a/app/assets/javascripts/ide/init_legacy_web_ide.js +++ /dev/null @@ -1,62 +0,0 @@ -// legacyWebIDE.js -import { identity } from 'lodash'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import { mapActions } from 'vuex'; -import { parseBoolean } from '../lib/utils/common_utils'; -import { DEFAULT_BRANCH } from './constants'; -import ide from './components/ide.vue'; -import { createRouter } from './ide_router'; -import { DEFAULT_THEME } from './lib/themes'; -import { createStore } from './stores'; - -export const initLegacyWebIDE = (el, options = {}) => { - if (!el) return null; - - const { rootComponent = ide, extendStore = identity } = options; - - const store = createStore(); - const project = JSON.parse(el.dataset.project); - store.dispatch('setProject', { project }); - - // fire and forget fetching non-critical project info - store.dispatch('fetchProjectPermissions'); - - const router = createRouter(store, el.dataset.defaultBranch || DEFAULT_BRANCH); - - return new Vue({ - el, - store: extendStore(store, el), - router, - created() { - this.setEmptyStateSvgs({ - emptyStateSvgPath: el.dataset.emptyStateSvgPath, - noChangesStateSvgPath: el.dataset.noChangesStateSvgPath, - committedStateSvgPath: el.dataset.committedStateSvgPath, - pipelinesEmptyStateSvgPath: el.dataset.pipelinesEmptyStateSvgPath, - switchEditorSvgPath: el.dataset.switchEditorSvgPath, - }); - this.setLinks({ - webIDEHelpPagePath: el.dataset.webIdeHelpPagePath, - newWebIDEHelpPagePath: el.dataset.newWebIdeHelpPagePath, - forkInfo: el.dataset.forkInfo ? JSON.parse(el.dataset.forkInfo) : null, - }); - this.init({ - renderWhitespaceInCode: parseBoolean(el.dataset.renderWhitespaceInCode), - editorTheme: window.gon?.user_color_scheme || DEFAULT_THEME, - previewMarkdownPath: el.dataset.previewMarkdownPath, - userPreferencesPath: el.dataset.userPreferencesPath, - }); - }, - beforeDestroy() { - // This helps tests do Singleton cleanups which we don't really have responsibility to know about here. - this.$emit('destroy'); - }, - methods: { - ...mapActions(['setEmptyStateSvgs', 'setLinks', 'init']), - }, - render(createElement) { - return createElement(rootComponent); - }, - }); -}; diff --git a/app/assets/javascripts/ide/lib/common/disposable.js b/app/assets/javascripts/ide/lib/common/disposable.js deleted file mode 100644 index c5d0773c9a2..00000000000 --- a/app/assets/javascripts/ide/lib/common/disposable.js +++ /dev/null @@ -1,14 +0,0 @@ -export default class Disposable { - constructor() { - this.disposers = new Set(); - } - - add(...disposers) { - disposers.forEach((disposer) => this.disposers.add(disposer)); - } - - dispose() { - this.disposers.forEach((disposer) => disposer.dispose()); - this.disposers.clear(); - } -} diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js deleted file mode 100644 index 5f67eee5f18..00000000000 --- a/app/assets/javascripts/ide/lib/common/model.js +++ /dev/null @@ -1,136 +0,0 @@ -import { editor as monacoEditor, Uri } from 'monaco-editor'; -import { insertFinalNewline } from '~/lib/utils/text_utility'; -import eventHub from '../../eventhub'; -import { trimTrailingWhitespace } from '../../utils'; -import { defaultModelOptions } from '../editor_options'; -import Disposable from './disposable'; - -export default class Model { - constructor(file, head = null) { - this.disposable = new Disposable(); - this.file = file; - this.head = head; - this.content = file.content !== '' || file.deleted ? file.content : file.raw; - this.options = { ...defaultModelOptions }; - - this.disposable.add( - (this.originalModel = monacoEditor.createModel( - head ? head.content : this.file.raw, - undefined, - new Uri('gitlab', false, `original/${this.path}`), - )), - (this.model = monacoEditor.createModel( - this.content, - undefined, - new Uri('gitlab', false, this.path), - )), - ); - if (this.file.mrChange) { - this.disposable.add( - (this.baseModel = monacoEditor.createModel( - this.file.baseRaw, - undefined, - new Uri('gitlab', false, `target/${this.path}`), - )), - ); - } - - this.events = new Set(); - - this.updateContent = this.updateContent.bind(this); - this.updateNewContent = this.updateNewContent.bind(this); - this.dispose = this.dispose.bind(this); - - eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose); - eventHub.$on(`editor.update.model.content.${this.file.key}`, this.updateContent); - eventHub.$on(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent); - } - - get url() { - return this.model.uri.toString(); - } - - get language() { - return this.model.getLanguageId(); - } - - get path() { - return this.file.key; - } - - getModel() { - return this.model; - } - - getOriginalModel() { - return this.originalModel; - } - - getBaseModel() { - return this.baseModel; - } - - setValue(value) { - this.getModel().setValue(value); - } - - onChange(cb) { - this.events.add(this.disposable.add(this.model.onDidChangeContent((e) => cb(this, e)))); - } - - onDispose(cb) { - this.events.add(cb); - } - - updateContent({ content, changed }) { - this.getOriginalModel().setValue(content); - - if (!changed) { - this.getModel().setValue(content); - } - } - - updateNewContent(content) { - this.getModel().setValue(content); - } - - updateOptions(obj = {}) { - Object.assign(this.options, obj); - this.model.updateOptions(obj); - this.applyCustomOptions(); - } - - applyCustomOptions() { - this.updateNewContent( - Object.entries(this.options).reduce((content, [key, value]) => { - switch (key) { - case 'endOfLine': - this.model.pushEOL(value); - return this.model.getValue(); - case 'insertFinalNewline': - return value ? insertFinalNewline(content) : content; - case 'trimTrailingWhitespace': - return value ? trimTrailingWhitespace(content) : content; - default: - return content; - } - }, this.model.getValue()), - ); - } - - dispose() { - if (!this.model.isDisposed()) this.applyCustomOptions(); - - this.events.forEach((cb) => { - if (typeof cb === 'function') cb(); - }); - - this.events.clear(); - - eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose); - eventHub.$off(`editor.update.model.content.${this.file.key}`, this.updateContent); - eventHub.$off(`editor.update.model.new.content.${this.file.key}`, this.updateNewContent); - - this.disposable.dispose(); - } -} diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js deleted file mode 100644 index bd9b8fc3fcc..00000000000 --- a/app/assets/javascripts/ide/lib/common/model_manager.js +++ /dev/null @@ -1,47 +0,0 @@ -import eventHub from '../../eventhub'; -import Disposable from './disposable'; -import Model from './model'; - -export default class ModelManager { - constructor() { - this.disposable = new Disposable(); - this.models = new Map(); - } - - hasCachedModel(key) { - return this.models.has(key); - } - - getModel(key) { - return this.models.get(key); - } - - addModel(file, head = null) { - if (this.hasCachedModel(file.key)) { - return this.getModel(file.key); - } - - const model = new Model(file, head); - this.models.set(model.path, model); - this.disposable.add(model); - - eventHub.$on( - `editor.update.model.dispose.${file.key}`, - this.removeCachedModel.bind(this, file), - ); - - return model; - } - - removeCachedModel(file) { - this.models.delete(file.key); - - eventHub.$off(`editor.update.model.dispose.${file.key}`, this.removeCachedModel); - } - - dispose() { - // dispose of all the models - this.disposable.dispose(); - this.models.clear(); - } -} diff --git a/app/assets/javascripts/ide/lib/create_diff.js b/app/assets/javascripts/ide/lib/create_diff.js deleted file mode 100644 index 17779b6314e..00000000000 --- a/app/assets/javascripts/ide/lib/create_diff.js +++ /dev/null @@ -1,87 +0,0 @@ -import { commitActionTypes } from '~/ide/constants'; -import { commitActionForFile } from '~/ide/stores/utils'; -import createFileDiff from './create_file_diff'; - -const getDeletedParents = (entries, file) => { - const parent = file.parentPath && entries[file.parentPath]; - - if (parent && parent.deleted) { - return [parent, ...getDeletedParents(entries, parent)]; - } - - return []; -}; - -const filesWithChanges = ({ stagedFiles = [], changedFiles = [], entries = {} }) => { - // We need changed files to overwrite staged, so put them at the end. - const changes = stagedFiles.concat(changedFiles).reduce((acc, file) => { - const key = file.path; - const action = commitActionForFile(file); - const prev = acc[key]; - - // If a file was deleted, which was previously added, then we should do nothing. - if (action === commitActionTypes.delete && prev && prev.action === commitActionTypes.create) { - delete acc[key]; - } else { - acc[key] = { action, file }; - } - - return acc; - }, {}); - - // We need to clean "move" actions, because we can only support 100% similarity moves at the moment. - // This is because the previous file's content might not be loaded. - Object.values(changes) - .filter((change) => change.action === commitActionTypes.move) - .forEach((change) => { - const prev = changes[change.file.prevPath]; - - if (!prev) { - return; - } - - if (change.file.content === prev.file.content) { - // If content is the same, continue with the move but don't do the prevPath's delete. - delete changes[change.file.prevPath]; - } else { - // Otherwise, treat the move as a delete / create. - Object.assign(change, { action: commitActionTypes.create }); - } - }); - - // Next, we need to add deleted directories by looking at the parents - Object.values(changes) - .filter((change) => change.action === commitActionTypes.delete && change.file.parentPath) - .forEach(({ file }) => { - // Do nothing if we've already visited this directory. - if (changes[file.parentPath]) { - return; - } - - getDeletedParents(entries, file).forEach((parent) => { - changes[parent.path] = { action: commitActionTypes.delete, file: parent }; - }); - }); - - return Object.values(changes); -}; - -const createDiff = (state) => { - const changes = filesWithChanges(state); - - const toDelete = changes - .filter((x) => x.action === commitActionTypes.delete) - .map((x) => x.file.path); - - const patch = changes - .filter((x) => x.action !== commitActionTypes.delete) - .map(({ file, action }) => createFileDiff(file, action)) - .join(''); - - return { - patch, - toDelete, - }; -}; - -export default createDiff; diff --git a/app/assets/javascripts/ide/lib/create_file_diff.js b/app/assets/javascripts/ide/lib/create_file_diff.js deleted file mode 100644 index b417b4765d8..00000000000 --- a/app/assets/javascripts/ide/lib/create_file_diff.js +++ /dev/null @@ -1,112 +0,0 @@ -/* eslint-disable @gitlab/require-i18n-strings */ -import { createTwoFilesPatch } from 'diff'; -import { commitActionTypes } from '~/ide/constants'; - -const DEV_NULL = '/dev/null'; -const DEFAULT_MODE = '100644'; -const NO_NEW_LINE = '\\ No newline at end of file'; -const NEW_LINE = '\n'; - -/** - * Cleans patch generated by `diff` package. - * - * - Removes "=======" separator added at the beginning - */ -const cleanTwoFilesPatch = (text) => text.replace(/^(=+\s*)/, ''); - -const endsWithNewLine = (val) => !val || val[val.length - 1] === NEW_LINE; - -const addEndingNewLine = (val) => (endsWithNewLine(val) ? val : val + NEW_LINE); - -const removeEndingNewLine = (val) => (endsWithNewLine(val) ? val.substr(0, val.length - 1) : val); - -const diffHead = (prevPath, newPath = '') => - `diff --git "a/${prevPath}" "b/${newPath || prevPath}"`; - -const createDiffBody = (path, content, isCreate) => { - if (!content) { - return ''; - } - - const prefix = isCreate ? '+' : '-'; - const fromPath = isCreate ? DEV_NULL : `a/${path}`; - const toPath = isCreate ? `b/${path}` : DEV_NULL; - - const hasNewLine = endsWithNewLine(content); - const lines = removeEndingNewLine(content).split(NEW_LINE); - - const chunkHead = isCreate ? `@@ -0,0 +1,${lines.length} @@` : `@@ -1,${lines.length} +0,0 @@`; - const chunk = lines - .map((line) => `${prefix}${line}`) - .concat(!hasNewLine ? [NO_NEW_LINE] : []) - .join(NEW_LINE); - - return `--- ${fromPath} -+++ ${toPath} -${chunkHead} -${chunk}`; -}; - -const createMoveFileDiff = (prevPath, newPath) => `${diffHead(prevPath, newPath)} -rename from ${prevPath} -rename to ${newPath}`; - -const createNewFileDiff = (path, content) => { - const diff = createDiffBody(path, content, true); - - return `${diffHead(path)} -new file mode ${DEFAULT_MODE} -${diff}`; -}; - -const createDeleteFileDiff = (path, content) => { - const diff = createDiffBody(path, content, false); - - return `${diffHead(path)} -deleted file mode ${DEFAULT_MODE} -${diff}`; -}; - -const createUpdateFileDiff = (path, oldContent, newContent) => { - const patch = createTwoFilesPatch(`a/${path}`, `b/${path}`, oldContent, newContent); - - return `${diffHead(path)} -${cleanTwoFilesPatch(patch)}`; -}; - -const createFileDiffRaw = (file, action) => { - switch (action) { - case commitActionTypes.move: - return createMoveFileDiff(file.prevPath, file.path); - case commitActionTypes.create: - return createNewFileDiff(file.path, file.content); - case commitActionTypes.delete: - return createDeleteFileDiff(file.path, file.content); - case commitActionTypes.update: - return createUpdateFileDiff(file.path, file.raw || '', file.content); - default: - return ''; - } -}; - -/** - * Create a git diff for a single IDE file. - * - * ## Notes: - * When called with `commitActionType.move`, it assumes that the move - * is a 100% similarity move. No diff will be generated. This is because - * generating a move with changes is not support by the current IDE, since - * the source file might not have it's content loaded yet. - * - * When called with `commitActionType.delete`, it does not support - * deleting files with a mode different than 100644. For the IDE mirror, this - * isn't needed because deleting is handled outside the unified patch. - * - * ## References: - * - https://git-scm.com/docs/git-diff#_generating_patches_with_p - */ -const createFileDiff = (file, action) => - // It's important that the file diff ends in a new line - git expects this. - addEndingNewLine(createFileDiffRaw(file, action)); - -export default createFileDiff; diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js deleted file mode 100644 index b5d3eb10952..00000000000 --- a/app/assets/javascripts/ide/lib/decorations/controller.js +++ /dev/null @@ -1,54 +0,0 @@ -export default class DecorationsController { - constructor(editor) { - this.editor = editor; - this.decorations = new Map(); - this.editorDecorations = new Map(); - } - - getAllDecorationsForModel(model) { - if (!this.decorations.has(model.url)) return []; - - const modelDecorations = this.decorations.get(model.url); - const decorations = []; - - modelDecorations.forEach((val) => decorations.push(...val)); - - return decorations; - } - - addDecorations(model, decorationsKey, decorations) { - const decorationMap = this.decorations.get(model.url) || new Map(); - - decorationMap.set(decorationsKey, decorations); - - this.decorations.set(model.url, decorationMap); - - this.decorate(model); - } - - decorate(model) { - if (!this.editor.instance) return; - - const decorations = this.getAllDecorationsForModel(model); - const oldDecorations = this.editorDecorations.get(model.url) || []; - - this.editorDecorations.set( - model.url, - this.editor.instance.deltaDecorations(oldDecorations, decorations), - ); - } - - hasDecorations(model) { - return this.decorations.has(model.url); - } - - removeDecorations(model) { - this.decorations.delete(model.url); - this.editorDecorations.delete(model.url); - } - - dispose() { - this.decorations.clear(); - this.editorDecorations.clear(); - } -} diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js deleted file mode 100644 index ec28845d805..00000000000 --- a/app/assets/javascripts/ide/lib/diff/controller.js +++ /dev/null @@ -1,89 +0,0 @@ -import { throttle } from 'lodash'; -import { Range } from 'monaco-editor'; -import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; -import Disposable from '../common/disposable'; -import DirtyDiffWorker from './diff_worker?worker'; - -export const getDiffChangeType = (change) => { - if (change.modified) { - return 'modified'; - } - if (change.added) { - return 'added'; - } - if (change.removed) { - return 'removed'; - } - - return ''; -}; - -export const getDecorator = (change) => ({ - range: new Range(change.lineNumber, 1, change.endLineNumber, 1), - options: { - isWholeLine: true, - linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`, - }, -}); - -export default class DirtyDiffController { - constructor(modelManager, decorationsController) { - this.disposable = new Disposable(); - this.models = new Map(); - this.editorSimpleWorker = null; - this.modelManager = modelManager; - this.decorationsController = decorationsController; - this.dirtyDiffWorker = new DirtyDiffWorker(); - this.throttledComputeDiff = throttle(this.computeDiff, DEFAULT_DEBOUNCE_AND_THROTTLE_MS); - this.decorate = this.decorate.bind(this); - - this.dirtyDiffWorker.addEventListener('message', this.decorate); - } - - attachModel(model) { - if (this.models.has(model.url)) return; - - model.onChange(() => this.throttledComputeDiff(model)); - model.onDispose(() => { - this.decorationsController.removeDecorations(model); - this.models.delete(model.url); - }); - - this.models.set(model.url, model); - } - - computeDiff(model) { - const originalModel = model.getOriginalModel(); - const newModel = model.getModel(); - - if (originalModel.isDisposed() || newModel.isDisposed()) return; - - this.dirtyDiffWorker.postMessage({ - path: model.path, - originalContent: originalModel.getValue(), - newContent: newModel.getValue(), - }); - } - - reDecorate(model) { - if (this.decorationsController.hasDecorations(model)) { - this.decorationsController.decorate(model); - } else { - this.computeDiff(model); - } - } - - decorate({ data }) { - const decorations = data.changes.map((change) => getDecorator(change)); - const model = this.modelManager.getModel(data.path); - this.decorationsController.addDecorations(model, 'dirtyDiff', decorations); - } - - dispose() { - this.disposable.dispose(); - this.models.clear(); - - this.dirtyDiffWorker.removeEventListener('message', this.decorate); - this.dirtyDiffWorker.terminate(); - } -} diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js deleted file mode 100644 index 5a6401f56ec..00000000000 --- a/app/assets/javascripts/ide/lib/diff/diff.js +++ /dev/null @@ -1,36 +0,0 @@ -import { diffLines } from 'diff'; -import { defaultDiffOptions } from '../editor_options'; - -export const computeDiff = (originalContent, newContent) => { - // prevent EOL changes from highlighting the entire file - const changes = diffLines( - originalContent.replace(/\r\n/g, '\n'), - newContent.replace(/\r\n/g, '\n'), - defaultDiffOptions, - ); - - let lineNumber = 1; - return changes.reduce((acc, change) => { - const findOnLine = acc.find((c) => c.lineNumber === lineNumber); - - if (findOnLine) { - Object.assign(findOnLine, change, { - modified: true, - endLineNumber: lineNumber + change.count - 1, - }); - } else if ('added' in change || 'removed' in change) { - acc.push({ - ...change, - lineNumber, - modified: undefined, - endLineNumber: lineNumber + change.count - 1, - }); - } - - if (!change.removed) { - lineNumber += change.count; - } - - return acc; - }, []); -}; diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js deleted file mode 100644 index 78b2eab6399..00000000000 --- a/app/assets/javascripts/ide/lib/diff/diff_worker.js +++ /dev/null @@ -1,12 +0,0 @@ -import { computeDiff } from './diff'; - -// eslint-disable-next-line no-restricted-globals -self.addEventListener('message', (e) => { - const { data } = e; - - // eslint-disable-next-line no-restricted-globals - self.postMessage({ - path: data.path, - changes: computeDiff(data.originalContent, data.newContent), - }); -}); diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js deleted file mode 100644 index 80191f635a3..00000000000 --- a/app/assets/javascripts/ide/lib/editor.js +++ /dev/null @@ -1,241 +0,0 @@ -import { debounce } from 'lodash'; -import { editor as monacoEditor, KeyCode, KeyMod, Range } from 'monaco-editor'; -import { clearDomElement } from '~/editor/utils'; -import { registerLanguages } from '../utils'; -import Disposable from './common/disposable'; -import ModelManager from './common/model_manager'; -import DecorationsController from './decorations/controller'; -import DirtyDiffController from './diff/controller'; -import { editorOptions, defaultEditorOptions, defaultDiffEditorOptions } from './editor_options'; -import keymap from './keymap.json'; -import languages from './languages'; -import { themes } from './themes'; - -function setupThemes() { - themes.forEach((theme) => { - monacoEditor.defineTheme(theme.name, theme.data); - }); -} - -export default class Editor { - static create(...args) { - if (!this.editorInstance) { - this.editorInstance = new Editor(...args); - } - return this.editorInstance; - } - - constructor(store, options = {}) { - this.currentModel = null; - this.instance = null; - this.dirtyDiffController = null; - this.disposable = new Disposable(); - this.modelManager = new ModelManager(); - this.decorationsController = new DecorationsController(this); - this.options = { - ...defaultEditorOptions, - ...options, - }; - this.diffOptions = { - ...defaultDiffEditorOptions, - ...options, - }; - this.store = store; - - setupThemes(); - registerLanguages(...languages); - - this.debouncedUpdate = debounce(() => { - this.updateDimensions(); - }, 200); - } - - createInstance(domElement) { - if (!this.instance) { - clearDomElement(domElement); - - this.disposable.add( - (this.instance = monacoEditor.create(domElement, { - ...this.options, - })), - (this.dirtyDiffController = new DirtyDiffController( - this.modelManager, - this.decorationsController, - )), - ); - - this.addCommands(); - - window.addEventListener('resize', this.debouncedUpdate, false); - } - } - - createDiffInstance(domElement) { - if (!this.instance) { - clearDomElement(domElement); - - this.disposable.add( - (this.instance = monacoEditor.createDiffEditor(domElement, { - ...this.diffOptions, - renderSideBySide: Editor.renderSideBySide(domElement), - })), - ); - - this.addCommands(); - - window.addEventListener('resize', this.debouncedUpdate, false); - } - } - - createModel(file, head = null) { - return this.modelManager.addModel(file, head); - } - - attachModel(model) { - if (this.isDiffEditorType) { - this.instance.setModel({ - original: model.getOriginalModel(), - modified: model.getModel(), - }); - - return; - } - - this.instance.setModel(model.getModel()); - if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model); - - this.currentModel = model; - - this.instance.updateOptions( - editorOptions.reduce((acc, obj) => { - Object.keys(obj).forEach((key) => { - Object.assign(acc, { - [key]: obj[key](model), - }); - }); - return acc; - }, {}), - ); - - if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model); - } - - attachMergeRequestModel(model) { - this.instance.setModel({ - original: model.getBaseModel(), - modified: model.getModel(), - }); - - monacoEditor.createDiffNavigator(this.instance, { - alwaysRevealFirst: true, - }); - } - - clearEditor() { - if (this.instance) { - this.instance.setModel(null); - } - } - - dispose() { - window.removeEventListener('resize', this.debouncedUpdate); - - // catch any potential errors with disposing the error - // this is mainly for tests caused by elements not existing - try { - this.disposable.dispose(); - - this.instance = null; - } catch (e) { - this.instance = null; - - if (process.env.NODE_ENV !== 'test') { - // eslint-disable-next-line no-console - console.error(e); - } - } - } - - updateDimensions() { - if (this.instance) { - this.instance.layout(); - this.updateDiffView(); - } - } - - setPosition({ lineNumber, column }) { - this.instance.revealPositionInCenter({ - lineNumber, - column, - }); - this.instance.setPosition({ - lineNumber, - column, - }); - } - - onPositionChange(cb) { - if (!this.instance.onDidChangeCursorPosition) return; - - this.disposable.add(this.instance.onDidChangeCursorPosition((e) => cb(this.instance, e))); - } - - updateDiffView() { - if (!this.isDiffEditorType) return; - - this.instance.updateOptions({ - renderSideBySide: Editor.renderSideBySide(this.instance.getDomNode()), - }); - } - - replaceSelectedText(text) { - let selection = this.instance.getSelection(); - const range = new Range( - selection.startLineNumber, - selection.startColumn, - selection.endLineNumber, - selection.endColumn, - ); - - this.instance.executeEdits('', [{ range, text }]); - - selection = this.instance.getSelection(); - this.instance.setPosition({ lineNumber: selection.endLineNumber, column: selection.endColumn }); - } - - get isDiffEditorType() { - return this.instance.getEditorType() === 'vs.editor.IDiffEditor'; - } - - static renderSideBySide(domElement) { - return domElement.offsetWidth >= 700; - } - - addCommands() { - const { store } = this; - const getKeyCode = (key) => { - const monacoKeyMod = key.indexOf('KEY_') === 0; - - return monacoKeyMod ? KeyCode[key] : KeyMod[key]; - }; - - keymap.forEach((command) => { - const keybindings = command.bindings.map((binding) => { - const keys = binding.split('+'); - - // eslint-disable-next-line no-bitwise - return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]); - }); - - this.instance.addAction({ - id: command.id, - label: command.label, - keybindings, - run() { - store.dispatch(command.action.name, command.action.params); - return null; - }, - }); - }); - } -} diff --git a/app/assets/javascripts/ide/lib/editorconfig/parser.js b/app/assets/javascripts/ide/lib/editorconfig/parser.js deleted file mode 100644 index 2adc643a15b..00000000000 --- a/app/assets/javascripts/ide/lib/editorconfig/parser.js +++ /dev/null @@ -1,59 +0,0 @@ -import { parseString } from 'editorconfig/src/lib/ini'; -import minimatch from 'minimatch'; -import { getPathParents } from '../../utils'; - -const dirname = (path) => path.replace(/\.editorconfig$/, ''); - -function isRootConfig(config) { - return config.some(([pattern, rules]) => !pattern && rules?.root === 'true'); -} - -function getRulesForSection(path, [pattern, rules]) { - if (!pattern) { - return {}; - } - if (minimatch(path, pattern, { matchBase: true })) { - return rules; - } - - return {}; -} - -function getRulesWithConfigs(filePath, configFiles = [], rules = {}) { - if (!configFiles.length) return rules; - - const [{ content, path: configPath }, ...nextConfigs] = configFiles; - const configDir = dirname(configPath); - - if (!filePath.startsWith(configDir)) return rules; - - const parsed = parseString(content); - const isRoot = isRootConfig(parsed); - const relativeFilePath = filePath.slice(configDir.length); - - const sectionRules = parsed.reduce( - (acc, section) => Object.assign(acc, getRulesForSection(relativeFilePath, section)), - {}, - ); - - // prefer existing rules by overwriting to section rules - const result = Object.assign(sectionRules, rules); - - return isRoot ? result : getRulesWithConfigs(filePath, nextConfigs, result); -} - -export function getRulesWithTraversal(filePath, getFileContent) { - const editorconfigPaths = [ - ...getPathParents(filePath).map((x) => `${x}/.editorconfig`), - '.editorconfig', - ]; - - return Promise.all( - editorconfigPaths.map((path) => getFileContent(path).then((content) => ({ path, content }))), - ).then((results) => - getRulesWithConfigs( - filePath, - results.filter((x) => x.content), - ), - ); -} diff --git a/app/assets/javascripts/ide/lib/editorconfig/rules_mapper.js b/app/assets/javascripts/ide/lib/editorconfig/rules_mapper.js deleted file mode 100644 index 25ffa9a15be..00000000000 --- a/app/assets/javascripts/ide/lib/editorconfig/rules_mapper.js +++ /dev/null @@ -1,33 +0,0 @@ -import { isBoolean, isNumber } from 'lodash'; - -const map = (key, validValues) => (value) => - value in validValues ? { [key]: validValues[value] } : {}; - -const bool = (key) => (value) => (isBoolean(value) ? { [key]: value } : {}); - -const int = (key, isValid) => (value) => - isNumber(value) && isValid(value) ? { [key]: Math.trunc(value) } : {}; - -const rulesMapper = { - indent_style: map('insertSpaces', { tab: false, space: true }), - indent_size: int('tabSize', (n) => n > 0), - tab_width: int('tabSize', (n) => n > 0), - trim_trailing_whitespace: bool('trimTrailingWhitespace'), - end_of_line: map('endOfLine', { crlf: 1, lf: 0 }), - insert_final_newline: bool('insertFinalNewline'), -}; - -const parseValue = (x) => { - let value = typeof x === 'string' ? x.toLowerCase() : x; - if (/^[0-9.-]+$/.test(value)) value = Number(value); - if (value === 'true') value = true; - if (value === 'false') value = false; - - return value; -}; - -export default function mapRulesToMonaco(rules) { - return Object.entries(rules).reduce((obj, [key, value]) => { - return Object.assign(obj, rulesMapper[key]?.(parseValue(value)) || {}); - }, {}); -} diff --git a/app/assets/javascripts/ide/lib/errors.js b/app/assets/javascripts/ide/lib/errors.js deleted file mode 100644 index 5063cf5fd4f..00000000000 --- a/app/assets/javascripts/ide/lib/errors.js +++ /dev/null @@ -1,67 +0,0 @@ -import { escape } from 'lodash'; -import { __ } from '~/locale'; -import { COMMIT_TO_NEW_BRANCH } from '../stores/modules/commit/constants'; - -const CODEOWNERS_REGEX = /Push.*protected branches.*CODEOWNERS/; -const BRANCH_CHANGED_REGEX = /changed.*since.*start.*edit/; -const BRANCH_ALREADY_EXISTS = /branch.*already.*exists/; - -const createNewBranchAndCommit = (store) => - store - .dispatch('commit/updateCommitAction', COMMIT_TO_NEW_BRANCH) - .then(() => store.dispatch('commit/commitChanges')); - -export const createUnexpectedCommitError = (message) => ({ - title: __('Unexpected error'), - messageHTML: escape(message) || __('Could not commit. An unexpected error occurred.'), -}); - -export const createCodeownersCommitError = (message) => ({ - title: __('CODEOWNERS rule violation'), - messageHTML: escape(message), - primaryAction: { - text: __('Create new branch'), - callback: createNewBranchAndCommit, - }, -}); - -export const createBranchChangedCommitError = (message) => ({ - title: __('Branch changed'), - messageHTML: `${escape(message)}

${__('Would you like to create a new branch?')}`, - primaryAction: { - text: __('Create new branch'), - callback: createNewBranchAndCommit, - }, -}); - -export const branchAlreadyExistsCommitError = (message) => ({ - title: __('Branch already exists'), - messageHTML: `${escape(message)}

${__( - 'Would you like to try auto-generating a branch name?', - )}`, - primaryAction: { - text: __('Create new branch'), - callback: (store) => - store.dispatch('commit/addSuffixToBranchName').then(() => createNewBranchAndCommit(store)), - }, -}); - -export const parseCommitError = (e) => { - const { message } = e?.response?.data || {}; - - if (!message) { - return createUnexpectedCommitError(); - } - - if (CODEOWNERS_REGEX.test(message)) { - return createCodeownersCommitError(message); - } - if (BRANCH_CHANGED_REGEX.test(message)) { - return createBranchChangedCommitError(message); - } - if (BRANCH_ALREADY_EXISTS.test(message)) { - return branchAlreadyExistsCommitError(message); - } - - return createUnexpectedCommitError(message); -}; diff --git a/app/assets/javascripts/ide/lib/files.js b/app/assets/javascripts/ide/lib/files.js deleted file mode 100644 index 415e34f56b8..00000000000 --- a/app/assets/javascripts/ide/lib/files.js +++ /dev/null @@ -1,110 +0,0 @@ -import { decorateData, sortTree } from '../stores/utils'; - -export const splitParent = (path) => { - const idx = path.lastIndexOf('/'); - - return { - parent: idx >= 0 ? path.substring(0, idx) : null, - name: idx >= 0 ? path.substring(idx + 1) : path, - }; -}; - -/** - * Create file objects from a list of file paths. - * - * @param {Array} options.data Array of blob paths to parse and create a file tree from. - * @param {Boolean} options.tempFile Web IDE flag for whether this is a "new" file or not. - * @param {String} options.content Content to initialize the new blob with. - * @param {String} options.rawPath Raw path used for the new blob. - * @param {Object} options.blobData Extra values to initialize each blob with. - */ -export const decorateFiles = ({ - data, - tempFile = false, - content = '', - rawPath = '', - blobData = {}, -}) => { - const treeList = []; - const entries = {}; - - // These mutable variable references end up being exported and used by `createTempEntry` - let file; - let parentPath; - - const insertParent = (path) => { - if (!path) { - return null; - } - if (entries[path]) { - return entries[path]; - } - - const { parent, name } = splitParent(path); - const parentFolder = parent && insertParent(parent); - parentPath = parentFolder && parentFolder.path; - - const tree = decorateData({ - id: path, - name, - path, - type: 'tree', - tempFile, - changed: tempFile, - opened: tempFile, - parentPath, - }); - - Object.assign(entries, { - [path]: tree, - }); - - if (parentFolder) { - parentFolder.tree.push(tree); - } else { - treeList.push(tree); - } - - return tree; - }; - - data.forEach((path) => { - const { parent, name } = splitParent(path); - - const fileFolder = parent && insertParent(parent); - - if (name) { - parentPath = fileFolder && fileFolder.path; - - file = decorateData({ - id: path, - name, - path, - type: 'blob', - tempFile, - changed: tempFile, - content, - rawPath, - parentPath, - ...blobData, - }); - - Object.assign(entries, { - [path]: file, - }); - - if (fileFolder) { - fileFolder.tree.push(file); - } else { - treeList.push(file); - } - } - }); - - return { - entries, - treeList: sortTree(treeList), - file, - parentPath, - }; -}; diff --git a/app/assets/javascripts/ide/lib/keymap.json b/app/assets/javascripts/ide/lib/keymap.json deleted file mode 100644 index 2db87c07dde..00000000000 --- a/app/assets/javascripts/ide/lib/keymap.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - "id": "file-finder", - "label": "File finder", - "bindings": ["CtrlCmd+KEY_P"], - "action": { - "name": "toggleFileFinder", - "params": true - } - }, - { - "id": "save-files", - "label": "Save files", - "bindings": ["CtrlCmd+KEY_S"], - "action": { - "name": "triggerFilesChange" - } - } -] diff --git a/app/assets/javascripts/ide/lib/mirror.js b/app/assets/javascripts/ide/lib/mirror.js deleted file mode 100644 index 286798d7560..00000000000 --- a/app/assets/javascripts/ide/lib/mirror.js +++ /dev/null @@ -1,156 +0,0 @@ -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { getWebSocketUrl, mergeUrlParams } from '~/lib/utils/url_utility'; -import { __ } from '~/locale'; -import createDiff from './create_diff'; - -export const SERVICE_NAME = 'webide-file-sync'; -export const PROTOCOL = 'webfilesync.gitlab.com'; -export const MSG_CONNECTION_ERROR = __('Could not connect to Web IDE file mirror service.'); - -// Before actually connecting to the service, we must delay a bit -// so that the service has sufficiently started. - -const noop = () => {}; -export const SERVICE_DELAY = 8000; - -const cancellableWait = (time) => { - let timeoutId = 0; - - const cancel = () => clearTimeout(timeoutId); - - const promise = new Promise((resolve) => { - timeoutId = setTimeout(resolve, time); - }); - - return [promise, cancel]; -}; - -const isErrorResponse = (error) => error && error.code !== 0; - -const isErrorPayload = (payload) => payload && payload.status_code !== HTTP_STATUS_OK; - -const getErrorFromResponse = (data) => { - if (isErrorResponse(data.error)) { - return { message: data.error.Message }; - } - if (isErrorPayload(data.payload)) { - return { message: data.payload.error_message }; - } - - return null; -}; - -const getFullPath = (path) => mergeUrlParams({ service: SERVICE_NAME }, getWebSocketUrl(path)); - -const createWebSocket = (fullPath) => - new Promise((resolve, reject) => { - const socket = new WebSocket(fullPath, [PROTOCOL]); - const resetCallbacks = () => { - socket.onopen = null; - socket.onerror = null; - }; - - socket.onopen = () => { - resetCallbacks(); - resolve(socket); - }; - - socket.onerror = () => { - resetCallbacks(); - reject(new Error(MSG_CONNECTION_ERROR)); - }; - }); - -export const canConnect = ({ services = [] }) => services.some((name) => name === SERVICE_NAME); - -export const createMirror = () => { - let socket = null; - let cancelHandler = noop; - let nextMessageHandler = noop; - - const cancelConnect = () => { - cancelHandler(); - cancelHandler = noop; - }; - - const onCancelConnect = (fn) => { - cancelHandler = fn; - }; - - const receiveMessage = (ev) => { - const handle = nextMessageHandler; - nextMessageHandler = noop; - handle(JSON.parse(ev.data)); - }; - - const onNextMessage = (fn) => { - nextMessageHandler = fn; - }; - - const waitForNextMessage = () => - new Promise((resolve, reject) => { - onNextMessage((data) => { - const err = getErrorFromResponse(data); - - if (err) { - reject(err); - } else { - resolve(); - } - }); - }); - - const uploadDiff = ({ toDelete, patch }) => { - if (!socket) { - return Promise.resolve(); - } - - const response = waitForNextMessage(); - - const msg = { - code: 'EVENT', - namespace: '/files', - event: 'PATCH', - payload: { diff: patch, delete_files: toDelete }, - }; - - socket.send(JSON.stringify(msg)); - - return response; - }; - - return { - upload(state) { - return uploadDiff(createDiff(state)); - }, - connect(path) { - if (socket) { - this.disconnect(); - } - - const fullPath = getFullPath(path); - const [wait, cancelWait] = cancellableWait(SERVICE_DELAY); - - onCancelConnect(cancelWait); - - return wait - .then(() => createWebSocket(fullPath)) - .then((newSocket) => { - socket = newSocket; - socket.onmessage = receiveMessage; - }); - }, - disconnect() { - cancelConnect(); - - if (!socket) { - return; - } - - socket.close(); - socket = null; - }, - }; -}; - -export default createMirror(); diff --git a/app/assets/javascripts/ide/messages.js b/app/assets/javascripts/ide/messages.js deleted file mode 100644 index fe8eba823a8..00000000000 --- a/app/assets/javascripts/ide/messages.js +++ /dev/null @@ -1,25 +0,0 @@ -import { s__ } from '~/locale'; - -export const MSG_CANNOT_PUSH_CODE_SHOULD_FORK = s__( - 'WebIDE|You can’t edit files directly in this project. Fork this project and submit a merge request with your changes.', -); - -export const MSG_CANNOT_PUSH_CODE_GO_TO_FORK = s__( - 'WebIDE|You can’t edit files directly in this project. Go to your fork and submit a merge request with your changes.', -); - -export const MSG_CANNOT_PUSH_CODE = s__( - 'WebIDE|You need permission to edit files directly in this project.', -); - -export const MSG_CANNOT_PUSH_UNSIGNED = s__( - 'WebIDE|This project does not accept unsigned commits. You can’t commit changes through the Web IDE.', -); - -export const MSG_CANNOT_PUSH_UNSIGNED_SHORT = s__( - 'WebIDE|This project does not accept unsigned commits.', -); - -export const MSG_FORK = s__('WebIDE|Fork project'); - -export const MSG_GO_TO_FORK = s__('WebIDE|Go to fork'); diff --git a/app/assets/javascripts/ide/queries/get_ide_project.query.graphql b/app/assets/javascripts/ide/queries/get_ide_project.query.graphql deleted file mode 100644 index 6ceb36909f3..00000000000 --- a/app/assets/javascripts/ide/queries/get_ide_project.query.graphql +++ /dev/null @@ -1,7 +0,0 @@ -#import "~/ide/queries/ide_project.fragment.graphql" - -query getIdeProject($projectPath: ID!) { - project(fullPath: $projectPath) { - ...IdeProject - } -} diff --git a/app/assets/javascripts/ide/queries/ide_project.fragment.graphql b/app/assets/javascripts/ide/queries/ide_project.fragment.graphql deleted file mode 100644 index a0b520858e6..00000000000 --- a/app/assets/javascripts/ide/queries/ide_project.fragment.graphql +++ /dev/null @@ -1,8 +0,0 @@ -fragment IdeProject on Project { - id - userPermissions { - createMergeRequestIn - readMergeRequest - pushCode - } -} diff --git a/app/assets/javascripts/ide/services/gql.js b/app/assets/javascripts/ide/services/gql.js deleted file mode 100644 index c8c1031c0f3..00000000000 --- a/app/assets/javascripts/ide/services/gql.js +++ /dev/null @@ -1,21 +0,0 @@ -import { memoize } from 'lodash'; -import createGqClient, { fetchPolicies } from '~/lib/graphql'; - -/** - * Returns a memoized client - * - * We defer creating the client so that importing this module does not cause any side-effects. - * Creating the client immediately caused issues with miragejs where the gql client uses the - * real fetch() instead of the shimmed one. - */ -const getClient = memoize(() => - createGqClient( - {}, - { - fetchPolicy: fetchPolicies.NO_CACHE, - }, - ), -); - -export const query = (...args) => getClient().query(...args); -export const mutate = (...args) => getClient().mutate(...args); diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js index 6c77fed01cb..01f455a8f96 100644 --- a/app/assets/javascripts/ide/services/index.js +++ b/app/assets/javascripts/ide/services/index.js @@ -1,90 +1,7 @@ import Api from '~/api'; -import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import axios from '~/lib/utils/axios_utils'; -import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; -import { query } from './gql'; export default { - getFileData(endpoint) { - return axios.get(endpoint, { - params: { format: 'json', viewer: 'none' }, - }); - }, - getRawFileData(file) { - if (file.tempFile && !file.prevPath) { - return Promise.resolve(file.content); - } - - if (file.raw || !file.rawPath) { - return Promise.resolve(file.raw); - } - - const options = file.binary ? { responseType: 'arraybuffer' } : {}; - - return axios - .get(file.rawPath, { - transformResponse: [(f) => f], - ...options, - }) - .then(({ data }) => data); - }, - getBaseRawFileData(file, projectId, ref) { - if (file.tempFile || file.baseRaw) return Promise.resolve(file.baseRaw); - - // if files are renamed, their base path has changed - const filePath = - file.mrChange && file.mrChange.renamed_file ? file.mrChange.old_path : file.path; - - return axios - .get( - joinPaths( - gon.relative_url_root || '/', - projectId, - '-', - 'raw', - ref, - escapeFileUrl(filePath), - ), - { - transformResponse: [(f) => f], - }, - ) - .then(({ data }) => data); - }, - getProjectMergeRequests(projectId, params = {}) { - return Api.projectMergeRequests(projectId, params); - }, - getProjectMergeRequestData(projectId, mergeRequestId, params = {}) { - return Api.projectMergeRequest(projectId, mergeRequestId, params); - }, - getProjectMergeRequestChanges(projectId, mergeRequestId) { - return Api.projectMergeRequestChanges(projectId, mergeRequestId); - }, - getProjectMergeRequestVersions(projectId, mergeRequestId) { - return Api.projectMergeRequestVersions(projectId, mergeRequestId); - }, - getBranchData(projectId, currentBranchId) { - return Api.branchSingle(projectId, currentBranchId); - }, commit(projectId, payload) { return Api.commitMultiple(projectId, payload); }, - getFiles(projectPath, ref) { - const url = `${gon.relative_url_root}/${projectPath}/-/files/${ref}`; - return axios.get(url, { params: { format: 'json' } }); - }, - lastCommitPipelines({ getters }) { - const commitSha = getters.lastCommit.id; - return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha); - }, - getProjectPermissionsData(projectPath) { - return query({ - query: getIdeProject, - variables: { projectPath }, - }).then(({ data }) => ({ - ...data.project, - id: getIdFromGraphQLId(data.project.id), - })); - }, }; diff --git a/app/assets/javascripts/ide/services/terminals.js b/app/assets/javascripts/ide/services/terminals.js deleted file mode 100644 index 99121948196..00000000000 --- a/app/assets/javascripts/ide/services/terminals.js +++ /dev/null @@ -1,17 +0,0 @@ -import axios from '~/lib/utils/axios_utils'; -import { joinPaths } from '~/lib/utils/url_utility'; - -export const baseUrl = (projectPath) => - joinPaths(gon.relative_url_root || '', `/${projectPath}/ide_terminals`); - -export const checkConfig = (projectPath, branch) => - axios.post(`${baseUrl(projectPath)}/check_config`, { - branch, - format: 'json', - }); - -export const create = (projectPath, branch) => - axios.post(baseUrl(projectPath), { - branch, - format: 'json', - }); diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js deleted file mode 100644 index bb4b181c56d..00000000000 --- a/app/assets/javascripts/ide/stores/actions.js +++ /dev/null @@ -1,313 +0,0 @@ -import { escape } from 'lodash'; -import Vue from 'vue'; -import { createAlert } from '~/alert'; -import { HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status'; -import { visitUrl } from '~/lib/utils/url_utility'; -import { __, sprintf } from '~/locale'; -import { - WEBIDE_MARK_FETCH_BRANCH_DATA_START, - WEBIDE_MARK_FETCH_BRANCH_DATA_FINISH, - WEBIDE_MEASURE_FETCH_BRANCH_DATA, -} from '~/performance/constants'; -import { performanceMarkAndMeasure } from '~/performance/utils'; -import { stageKeys, commitActionTypes } from '../constants'; -import eventHub from '../eventhub'; -import { decorateFiles } from '../lib/files'; -import service from '../services'; -import * as types from './mutation_types'; - -export const redirectToUrl = (self, url) => visitUrl(url); - -export const init = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data); - -export const discardAllChanges = ({ state, commit, dispatch }) => { - state.changedFiles.forEach((file) => dispatch('restoreOriginalFile', file.path)); - - commit(types.REMOVE_ALL_CHANGES_FILES); -}; - -export const setResizingStatus = ({ commit }, resizing) => { - commit(types.SET_RESIZING_STATUS, resizing); -}; - -export const createTempEntry = ( - { state, commit, dispatch, getters }, - { name, type, content = '', rawPath = '', openFile = true, makeFileActive = true, mimeType = '' }, -) => { - const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name; - - if (getters.entryExists(name)) { - createAlert({ - message: sprintf(__('The name "%{name}" is already taken in this directory.'), { - name: name.split('/').pop(), - }), - fadeTransition: false, - addBodyClass: true, - }); - - return undefined; - } - - const data = decorateFiles({ - data: [fullName], - type, - tempFile: true, - content, - rawPath, - blobData: { - mimeType, - }, - }); - const { file, parentPath } = data; - - commit(types.CREATE_TMP_ENTRY, { data }); - - if (type === 'blob') { - if (openFile) commit(types.TOGGLE_FILE_OPEN, file.path); - commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }); - - if (openFile && makeFileActive) dispatch('setFileActive', file.path); - dispatch('triggerFilesChange'); - } - - if (parentPath && !state.entries[parentPath].opened) { - commit(types.TOGGLE_TREE_OPEN, parentPath); - } - - return file; -}; - -export const addTempImage = ({ dispatch, getters }, { name, rawPath = '', content = '' }) => - dispatch('createTempEntry', { - name: getters.getAvailableFileName(name), - type: 'blob', - content, - rawPath, - openFile: false, - makeFileActive: false, - }); - -export const scrollToTab = () => { - Vue.nextTick(() => { - const tabs = document.getElementById('tabs'); - - if (tabs) { - const tabEl = tabs.querySelector('.active .repo-tab'); - - tabEl.focus(); - } - }); -}; - -export const stageAllChanges = ({ state, commit, dispatch, getters }) => { - const openFile = state.openFiles[0]; - - commit(types.SET_LAST_COMMIT_MSG, ''); - - state.changedFiles.forEach((file) => - commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }), - ); - - const file = getters.getStagedFile(openFile.path); - - if (file) { - dispatch('openPendingTab', { - file, - keyPrefix: stageKeys.staged, - }); - } -}; - -export const unstageAllChanges = ({ state, commit, dispatch, getters }) => { - const openFile = state.openFiles[0]; - - state.stagedFiles.forEach((file) => - commit(types.UNSTAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) }), - ); - - const file = getters.getChangedFile(openFile.path); - - if (file) { - dispatch('openPendingTab', { - file, - keyPrefix: stageKeys.unstaged, - }); - } -}; - -export const updateViewer = ({ commit }, viewer) => { - commit(types.UPDATE_VIEWER, viewer); -}; - -export const updateDelayViewerUpdated = ({ commit }, delay) => { - commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay); -}; - -export const updateActivityBarView = ({ commit }, view) => { - commit(types.UPDATE_ACTIVITY_BAR_VIEW, view); -}; - -export const setEmptyStateSvgs = ({ commit }, svgs) => { - commit(types.SET_EMPTY_STATE_SVGS, svgs); -}; - -export const setCurrentBranchId = ({ commit }, currentBranchId) => { - commit(types.SET_CURRENT_BRANCH, currentBranchId); -}; - -export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => { - commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile }); - - const parent = file.parentPath && state.entries[file.parentPath]; - - if (parent) { - dispatch('updateTempFlagForEntry', { file: parent, tempFile }); - } -}; - -export const toggleFileFinder = ({ commit }, fileFindVisible) => - commit(types.TOGGLE_FILE_FINDER, fileFindVisible); - -export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links); - -export const setErrorMessage = ({ commit }, errorMessage) => - commit(types.SET_ERROR_MESSAGE, errorMessage); - -export const deleteEntry = ({ commit, dispatch, state }, path) => { - const entry = state.entries[path]; - const { prevPath, prevName, prevParentPath } = entry; - const isTree = entry.type === 'tree'; - const prevEntry = prevPath && state.entries[prevPath]; - - if (prevPath && (!prevEntry || prevEntry.deleted)) { - dispatch('renameEntry', { - path, - name: prevName, - parentPath: prevParentPath, - }); - dispatch('deleteEntry', prevPath); - return; - } - - if (entry.opened) dispatch('closeFile', entry); - - if (isTree) { - entry.tree.forEach((f) => dispatch('deleteEntry', f.path)); - } - - commit(types.DELETE_ENTRY, path); - - // Only stage if we're not a directory or a new file - if (!isTree && !entry.tempFile) { - dispatch('stageChange', path); - } - - dispatch('triggerFilesChange'); -}; - -export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); - -export const renameEntry = ({ dispatch, commit, state, getters }, { path, name, parentPath }) => { - const entry = state.entries[path]; - const newPath = parentPath ? `${parentPath}/${name}` : name; - const existingParent = parentPath && state.entries[parentPath]; - - if (parentPath && (!existingParent || existingParent.deleted)) { - dispatch('createTempEntry', { name: parentPath, type: 'tree' }); - } - - commit(types.RENAME_ENTRY, { path, name, parentPath }); - - if (entry.type === 'tree') { - state.entries[newPath].tree.forEach((f) => { - dispatch('renameEntry', { - path: f.path, - name: f.name, - parentPath: newPath, - }); - }); - } else { - const newEntry = state.entries[newPath]; - const isRevert = newPath === entry.prevPath; - const isReset = isRevert && !newEntry.changed && !newEntry.tempFile; - const isInChanges = state.changedFiles - .concat(state.stagedFiles) - .some(({ key }) => key === newEntry.key); - - if (isReset) { - commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry); - } else if (!isInChanges) { - commit(types.STAGE_CHANGE, { path: newPath, diffInfo: getters.getDiffInfo(newPath) }); - } - - if (!newEntry.tempFile) { - eventHub.$emit(`editor.update.model.dispose.${entry.key}`); - } - - if (newEntry.opened) { - dispatch('router/push', getters.getUrlForPath(newEntry.path), { root: true }); - } - } - - dispatch('triggerFilesChange', { type: commitActionTypes.move, path, newPath }); -}; - -export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) => { - return new Promise((resolve, reject) => { - performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_BRANCH_DATA_START }); - const currentProject = state.projects[projectId]; - if (!currentProject || !currentProject.branches[branchId] || force) { - service - .getBranchData(projectId, branchId) - .then(({ data }) => { - performanceMarkAndMeasure({ - mark: WEBIDE_MARK_FETCH_BRANCH_DATA_FINISH, - measures: [ - { - name: WEBIDE_MEASURE_FETCH_BRANCH_DATA, - start: WEBIDE_MARK_FETCH_BRANCH_DATA_START, - }, - ], - }); - const { id } = data.commit; - commit(types.SET_BRANCH, { - projectPath: projectId, - branchName: branchId, - branch: data, - }); - commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); - resolve(data); - }) - .catch((e) => { - if (e.response.status === HTTP_STATUS_NOT_FOUND) { - reject(e); - } else { - createAlert({ - message: __('Error loading branch data. Please try again.'), - fadeTransition: false, - addBodyClass: true, - }); - - reject( - new Error( - sprintf( - __('Branch not loaded - %{branchId}'), - { - branchId: `${escape(projectId)}/${escape(branchId)}`, - }, - false, - ), - ), - ); - } - }); - } else { - resolve(currentProject.branches[branchId]); - } - }); -}; - -export * from './actions/tree'; -export * from './actions/file'; -export * from './actions/project'; -export * from './actions/merge_request'; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js deleted file mode 100644 index d1e40920ebc..00000000000 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ /dev/null @@ -1,297 +0,0 @@ -import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility'; -import { __ } from '~/locale'; -import { - WEBIDE_MARK_FETCH_FILE_DATA_START, - WEBIDE_MARK_FETCH_FILE_DATA_FINISH, - WEBIDE_MEASURE_FETCH_FILE_DATA, -} from '~/performance/constants'; -import { performanceMarkAndMeasure } from '~/performance/utils'; -import { viewerTypes, stageKeys, commitActionTypes } from '../../constants'; -import eventHub from '../../eventhub'; -import service from '../../services'; -import * as types from '../mutation_types'; -import { setPageTitleForFile } from '../utils'; - -export const closeFile = ({ commit, state, dispatch, getters }, file) => { - const { path } = file; - const indexOfClosedFile = state.openFiles.findIndex((f) => f.key === file.key); - const fileWasActive = file.active; - - if (state.openFiles.length > 1 && fileWasActive) { - const nextIndexToOpen = indexOfClosedFile === 0 ? 1 : indexOfClosedFile - 1; - const nextFileToOpen = state.openFiles[nextIndexToOpen]; - - if (nextFileToOpen.pending) { - dispatch('updateViewer', viewerTypes.diff); - dispatch('openPendingTab', { - file: nextFileToOpen, - keyPrefix: nextFileToOpen.staged ? 'staged' : 'unstaged', - }); - } else { - dispatch('setFileActive', nextFileToOpen.path); - dispatch('router/push', getters.getUrlForPath(nextFileToOpen.path), { root: true }); - } - } else if (state.openFiles.length === 1) { - dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, { - root: true, - }); - } - - if (file.pending) { - commit(types.REMOVE_PENDING_TAB, file); - } else { - commit(types.TOGGLE_FILE_OPEN, path); - commit(types.SET_FILE_ACTIVE, { path, active: false }); - } - - eventHub.$emit(`editor.update.model.dispose.${file.key}`); -}; - -export const setFileActive = ({ commit, state, getters, dispatch }, path) => { - const file = state.entries[path]; - const currentActiveFile = getters.activeFile; - - if (file.active) return; - - if (currentActiveFile) { - commit(types.SET_FILE_ACTIVE, { - path: currentActiveFile.path, - active: false, - }); - } - - commit(types.SET_FILE_ACTIVE, { path, active: true }); - dispatch('scrollToTab'); -}; - -export const getFileData = ( - { state, commit, dispatch, getters }, - { path, makeFileActive = true, openFile = makeFileActive, toggleLoading = true }, -) => { - performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_FILE_DATA_START }); - const file = state.entries[path]; - const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path); - - if (file.raw || (file.tempFile && !file.prevPath && !fileDeletedAndReadded)) - return Promise.resolve(); - - commit(types.TOGGLE_LOADING, { entry: file, forceValue: true }); - - const url = joinPaths( - gon.relative_url_root || '/', - state.currentProjectId, - '-', - file.type, - getters.lastCommit && getters.lastCommit.id, - escapeFileUrl(file.prevPath || file.path), - ); - - return service - .getFileData(url) - .then(({ data }) => { - performanceMarkAndMeasure({ - mark: WEBIDE_MARK_FETCH_FILE_DATA_FINISH, - measures: [ - { - name: WEBIDE_MEASURE_FETCH_FILE_DATA, - start: WEBIDE_MARK_FETCH_FILE_DATA_START, - }, - ], - }); - if (data) commit(types.SET_FILE_DATA, { data, file }); - if (openFile) commit(types.TOGGLE_FILE_OPEN, path); - - if (makeFileActive) { - setPageTitleForFile(state, file); - dispatch('setFileActive', path); - } - }) - .catch(() => { - dispatch('setErrorMessage', { - text: __('An error occurred while loading the file.'), - action: (payload) => - dispatch('getFileData', payload).then(() => dispatch('setErrorMessage', null)), - actionText: __('Please try again'), - actionPayload: { path, makeFileActive }, - }); - }) - .finally(() => { - if (toggleLoading) commit(types.TOGGLE_LOADING, { entry: file, forceValue: false }); - }); -}; - -export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => { - const file = state.entries[path]; - const stagedFile = state.stagedFiles.find((f) => f.path === path); - - const fileDeletedAndReadded = getters.isFileDeletedAndReadded(path); - commit(types.TOGGLE_LOADING, { entry: file, forceValue: true }); - return service - .getRawFileData(fileDeletedAndReadded ? stagedFile : file) - .then((raw) => { - if (!(file.tempFile && !file.prevPath && !fileDeletedAndReadded)) - commit(types.SET_FILE_RAW_DATA, { file, raw, fileDeletedAndReadded }); - - if (file.mrChange && file.mrChange.new_file === false) { - const baseSha = - (getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || ''; - - return service.getBaseRawFileData(file, state.currentProjectId, baseSha).then((baseRaw) => { - commit(types.SET_FILE_BASE_RAW_DATA, { - file, - baseRaw, - }); - return raw; - }); - } - return raw; - }) - .catch((e) => { - dispatch('setErrorMessage', { - text: __('An error occurred while loading the file content.'), - action: (payload) => - dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)), - actionText: __('Please try again'), - actionPayload: { path }, - }); - throw e; - }) - .finally(() => { - commit(types.TOGGLE_LOADING, { entry: file, forceValue: false }); - }); -}; - -export const changeFileContent = ({ commit, dispatch, state, getters }, { path, content }) => { - const file = state.entries[path]; - - // It's possible for monaco to hit a race condition where it tries to update renamed files. - // See issue https://gitlab.com/gitlab-org/gitlab/-/issues/284930 - if (!file) { - return; - } - - commit(types.UPDATE_FILE_CONTENT, { - path, - content, - }); - - const indexOfChangedFile = state.changedFiles.findIndex((f) => f.path === path); - - if (file.changed && indexOfChangedFile === -1) { - commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) }); - } else if (!file.changed && !file.tempFile && indexOfChangedFile !== -1) { - commit(types.REMOVE_FILE_FROM_CHANGED, path); - } - - dispatch('triggerFilesChange', { type: commitActionTypes.update, path }); -}; - -export const restoreOriginalFile = ({ dispatch, state, commit }, path) => { - const file = state.entries[path]; - const isDestructiveDiscard = file.tempFile || file.prevPath; - - if (file.deleted && file.parentPath) { - dispatch('restoreTree', file.parentPath); - } - - if (isDestructiveDiscard) { - dispatch('closeFile', file); - } - - if (file.tempFile) { - dispatch('deleteEntry', file.path); - } else { - commit(types.DISCARD_FILE_CHANGES, file.path); - } - - if (file.prevPath) { - dispatch('renameEntry', { - path: file.path, - name: file.prevName, - parentPath: file.prevParentPath, - }); - } -}; - -export const discardFileChanges = ({ dispatch, state, commit, getters }, path) => { - const file = state.entries[path]; - const isDestructiveDiscard = file.tempFile || file.prevPath; - - dispatch('restoreOriginalFile', path); - - if (!isDestructiveDiscard && file.path === getters.activeFile?.path) { - dispatch('updateDelayViewerUpdated', true) - .then(() => { - dispatch('router/push', getters.getUrlForPath(file.path), { root: true }); - }) - .catch((e) => { - throw e; - }); - } - - commit(types.REMOVE_FILE_FROM_CHANGED, path); - - eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.content); - eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content); -}; - -export const stageChange = ({ commit, dispatch, getters }, path) => { - const stagedFile = getters.getStagedFile(path); - const openFile = getters.getOpenFile(path); - - commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) }); - commit(types.SET_LAST_COMMIT_MSG, ''); - - if (stagedFile) { - eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content); - } - - const file = getters.getStagedFile(path); - - if (openFile && openFile.active && file) { - dispatch('openPendingTab', { - file, - keyPrefix: stageKeys.staged, - }); - } -}; - -export const unstageChange = ({ commit, dispatch, getters }, path) => { - const openFile = getters.getOpenFile(path); - - commit(types.UNSTAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) }); - - const file = getters.getChangedFile(path); - - if (openFile && openFile.active && file) { - dispatch('openPendingTab', { - file, - keyPrefix: stageKeys.unstaged, - }); - } -}; - -export const openPendingTab = ({ commit, dispatch, getters, state }, { file, keyPrefix }) => { - if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false; - - state.openFiles.forEach((f) => eventHub.$emit(`editor.update.model.dispose.${f.key}`)); - - commit(types.ADD_PENDING_TAB, { file, keyPrefix }); - - dispatch('router/push', `/project/${state.currentProjectId}/tree/${state.currentBranchId}/`, { - root: true, - }); - - return true; -}; - -export const removePendingTab = ({ commit }, file) => { - commit(types.REMOVE_PENDING_TAB, file); - - eventHub.$emit(`editor.update.model.dispose.${file.key}`); -}; - -export const triggerFilesChange = (ctx, payload = {}) => { - // Used in EE for file mirroring - eventHub.$emit('ide.files.change', payload); -}; diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js deleted file mode 100644 index 10cda8d21c9..00000000000 --- a/app/assets/javascripts/ide/stores/actions/merge_request.js +++ /dev/null @@ -1,240 +0,0 @@ -import { createAlert } from '~/alert'; -import { STATUS_OPEN } from '~/issues/constants'; -import { sprintf, __ } from '~/locale'; -import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '../../constants'; -import service from '../../services'; -import * as types from '../mutation_types'; - -export const getMergeRequestsForBranch = ( - { commit, state, getters }, - { projectId, branchId } = {}, -) => { - if (!getters.findProjectPermissions(projectId)[PERMISSION_READ_MR]) { - return Promise.resolve(); - } - - return service - .getProjectMergeRequests(`${projectId}`, { - source_branch: branchId, - source_project_id: state.projects[projectId].id, - state: STATUS_OPEN, - order_by: 'created_at', - per_page: 1, - }) - .then(({ data }) => { - if (data.length > 0) { - const currentMR = data[0]; - - commit(types.SET_MERGE_REQUEST, { - projectPath: projectId, - mergeRequestId: currentMR.iid, - mergeRequest: currentMR, - }); - - commit(types.SET_CURRENT_MERGE_REQUEST, `${currentMR.iid}`); - } - }) - .catch((e) => { - createAlert({ - message: sprintf(__('Error fetching merge requests for %{branchId}'), { branchId }), - fadeTransition: false, - addBodyClass: true, - }); - throw e; - }); -}; - -export const getMergeRequestData = ( - { commit, dispatch, state }, - { projectId, mergeRequestId, targetProjectId = null, force = false } = {}, -) => - new Promise((resolve, reject) => { - if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) { - service - .getProjectMergeRequestData(targetProjectId || projectId, mergeRequestId) - .then(({ data }) => { - commit(types.SET_MERGE_REQUEST, { - projectPath: projectId, - mergeRequestId, - mergeRequest: data, - }); - commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId); - resolve(data); - }) - .catch(() => { - dispatch('setErrorMessage', { - text: __('An error occurred while loading the merge request.'), - action: (payload) => - dispatch('getMergeRequestData', payload).then(() => - dispatch('setErrorMessage', null), - ), - actionText: __('Please try again'), - actionPayload: { projectId, mergeRequestId, force }, - }); - reject(new Error(`Merge request not loaded ${projectId}`)); - }); - } else { - resolve(state.projects[projectId].mergeRequests[mergeRequestId]); - } - }); - -export const getMergeRequestChanges = ( - { commit, dispatch, state }, - { projectId, mergeRequestId, targetProjectId = null, force = false } = {}, -) => - new Promise((resolve, reject) => { - if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) { - service - .getProjectMergeRequestChanges(targetProjectId || projectId, mergeRequestId) - .then(({ data }) => { - commit(types.SET_MERGE_REQUEST_CHANGES, { - projectPath: projectId, - mergeRequestId, - changes: data, - }); - resolve(data); - }) - .catch(() => { - dispatch('setErrorMessage', { - text: __('An error occurred while loading the merge request changes.'), - action: (payload) => - dispatch('getMergeRequestChanges', payload).then(() => - dispatch('setErrorMessage', null), - ), - actionText: __('Please try again'), - actionPayload: { projectId, mergeRequestId, force }, - }); - reject(new Error(`Merge request changes not loaded ${projectId}`)); - }); - } else { - resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes); - } - }); - -export const getMergeRequestVersions = ( - { commit, dispatch, state }, - { projectId, mergeRequestId, targetProjectId = null, force = false } = {}, -) => - new Promise((resolve, reject) => { - if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) { - service - .getProjectMergeRequestVersions(targetProjectId || projectId, mergeRequestId) - .then((res) => res.data) - .then((data) => { - commit(types.SET_MERGE_REQUEST_VERSIONS, { - projectPath: projectId, - mergeRequestId, - versions: data, - }); - resolve(data); - }) - .catch(() => { - dispatch('setErrorMessage', { - text: __('An error occurred while loading the merge request version data.'), - action: (payload) => - dispatch('getMergeRequestVersions', payload).then(() => - dispatch('setErrorMessage', null), - ), - actionText: __('Please try again'), - actionPayload: { projectId, mergeRequestId, force }, - }); - reject(new Error(`Merge request versions not loaded ${projectId}`)); - }); - } else { - resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions); - } - }); - -export const openMergeRequestChanges = async ({ dispatch, getters, state, commit }, changes) => { - const entryChanges = changes - .map((change) => ({ entry: state.entries[change.new_path], change })) - .filter((x) => x.entry); - - const pathsToOpen = entryChanges - .slice(0, MAX_MR_FILES_AUTO_OPEN) - .map(({ change }) => change.new_path); - - // If there are no changes with entries, do nothing. - if (!entryChanges.length) { - return; - } - - dispatch('updateActivityBarView', leftSidebarViews.review.name); - - entryChanges.forEach(({ change, entry }) => { - commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file: entry, mrChange: change }); - }); - - // Open paths in order as they appear in MR changes - pathsToOpen.forEach((path) => { - commit(types.TOGGLE_FILE_OPEN, path); - }); - - // Activate first path. - // We don't `getFileData` here since the editor component kicks that off. Otherwise, we'd fetch twice. - const [firstPath, ...remainingPaths] = pathsToOpen; - await dispatch('router/push', getters.getUrlForPath(firstPath)); - await dispatch('setFileActive', firstPath); - - // Lastly, eagerly fetch the remaining paths for improved user experience. - await Promise.all( - remainingPaths.map(async (path) => { - try { - await dispatch('getFileData', { - path, - makeFileActive: false, - }); - await dispatch('getRawFileData', { path }); - } catch (e) { - // If one of the file fetches fails, we dont want to blow up the rest of them. - // eslint-disable-next-line no-console - console.error('[gitlab] An unexpected error occurred fetching MR file data', e); - } - }), - ); -}; - -export const openMergeRequest = async ( - { dispatch, getters }, - { projectId, targetProjectId, mergeRequestId } = {}, -) => { - try { - const mr = await dispatch('getMergeRequestData', { - projectId, - targetProjectId, - mergeRequestId, - }); - - dispatch('setCurrentBranchId', mr.source_branch); - - await dispatch('getBranchData', { - projectId, - branchId: mr.source_branch, - }); - - const branch = getters.findBranch(projectId, mr.source_branch); - - await dispatch('getFiles', { - projectId, - branchId: mr.source_branch, - ref: branch.commit.id, - }); - - await dispatch('getMergeRequestVersions', { - projectId, - targetProjectId, - mergeRequestId, - }); - - const { changes } = await dispatch('getMergeRequestChanges', { - projectId, - targetProjectId, - mergeRequestId, - }); - - await dispatch('openMergeRequestChanges', changes); - } catch (e) { - createAlert({ message: __('Error while loading the merge request. Please try again.') }); - throw e; - } -}; diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js deleted file mode 100644 index 11e3d8260f7..00000000000 --- a/app/assets/javascripts/ide/stores/actions/project.js +++ /dev/null @@ -1,182 +0,0 @@ -import { escape } from 'lodash'; -import { createAlert } from '~/alert'; -import { __, sprintf } from '~/locale'; -import { logError } from '~/lib/logger'; -import api from '~/api'; -import service from '../../services'; -import * as types from '../mutation_types'; - -const ERROR_LOADING_PROJECT = __('Error loading project data. Please try again.'); - -const errorFetchingData = (e) => { - logError(ERROR_LOADING_PROJECT, e); - - createAlert({ - message: ERROR_LOADING_PROJECT, - fadeTransition: false, - addBodyClass: true, - }); -}; - -export const setProject = ({ commit }, { project } = {}) => { - if (!project) { - return; - } - const projectPath = project.path_with_namespace; - commit(types.SET_PROJECT, { projectPath, project }); - commit(types.SET_CURRENT_PROJECT, projectPath); -}; - -export const fetchProjectPermissions = ({ commit, state }) => { - const projectPath = state.currentProjectId; - if (!projectPath) { - return undefined; - } - return service - .getProjectPermissionsData(projectPath) - .then((permissions) => { - commit(types.UPDATE_PROJECT, { projectPath, props: permissions }); - }) - .catch(errorFetchingData); -}; - -export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) => - service - .getBranchData(projectId, branchId) - .then(({ data }) => { - commit(types.SET_BRANCH_COMMIT, { - projectId, - branchId, - commit: data.commit, - }); - }) - .catch((e) => { - createAlert({ - message: __('Error loading last commit.'), - fadeTransition: false, - addBodyClass: true, - }); - throw e; - }); - -export const createNewBranchFromDefault = ({ state, dispatch, getters }, branch) => - api - .createBranch(state.currentProjectId, { - ref: getters.currentProject.default_branch, - branch, - }) - .then(() => { - dispatch('setErrorMessage', null); - window.location.reload(); - }) - .catch(() => { - dispatch('setErrorMessage', { - text: __('An error occurred creating the new branch.'), - action: (payload) => dispatch('createNewBranchFromDefault', payload), - actionText: __('Please try again'), - actionPayload: branch, - }); - }); - -export const showBranchNotFoundError = ({ dispatch }, branchId) => { - dispatch('setErrorMessage', { - text: sprintf( - __("Branch %{branchName} was not found in this project's repository."), - { - branchName: `${escape(branchId)}`, - }, - false, - ), - action: (payload) => dispatch('createNewBranchFromDefault', payload), - actionText: __('Create branch'), - actionPayload: branchId, - }); -}; - -export const loadEmptyBranch = ({ commit, state }, { projectId, branchId }) => { - const treePath = `${projectId}/${branchId}`; - const currentTree = state.trees[`${projectId}/${branchId}`]; - - // If we already have a tree, let's not recreate an empty one - if (currentTree) { - return; - } - - commit(types.CREATE_TREE, { treePath }); - commit(types.TOGGLE_LOADING, { - entry: state.trees[treePath], - forceValue: false, - }); -}; - -export const loadFile = ({ dispatch, state }, { basePath }) => { - if (basePath) { - const path = basePath.slice(-1) === '/' ? basePath.slice(0, -1) : basePath; - const treeEntryKey = Object.keys(state.entries).find( - (key) => key === path && !state.entries[key].pending, - ); - const treeEntry = state.entries[treeEntryKey]; - - if (treeEntry) { - dispatch('handleTreeEntryAction', treeEntry); - } else { - dispatch('createTempEntry', { - name: path, - type: 'blob', - }); - } - } -}; - -export const loadBranch = ({ dispatch, getters, state }, { projectId, branchId }) => { - const currentProject = state.projects[projectId]; - - if (currentProject?.branches?.[branchId]) { - return Promise.resolve(); - } - if (getters.emptyRepo) { - return dispatch('loadEmptyBranch', { projectId, branchId }); - } - - return dispatch('getBranchData', { - projectId, - branchId, - }) - .then(() => { - dispatch('getMergeRequestsForBranch', { - projectId, - branchId, - }); - - const branch = getters.findBranch(projectId, branchId); - - return dispatch('getFiles', { - projectId, - branchId, - ref: branch.commit.id, - }); - }) - .catch((err) => { - dispatch('showBranchNotFoundError', branchId); - throw err; - }); -}; - -export const openBranch = ({ dispatch }, { projectId, branchId, basePath }) => { - dispatch('setCurrentBranchId', branchId); - - return dispatch('loadBranch', { projectId, branchId }) - .then(() => dispatch('loadFile', { basePath })) - .catch( - () => - new Error( - sprintf( - __('An error occurred while getting files for - %{branchId}'), - { - branchId: `${escape(projectId)}/${escape(branchId)}`, - }, - false, - ), - ), - ); -}; diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js deleted file mode 100644 index 20d8dc3381d..00000000000 --- a/app/assets/javascripts/ide/stores/actions/tree.js +++ /dev/null @@ -1,114 +0,0 @@ -import { defer } from 'lodash'; -import { - WEBIDE_MARK_FETCH_FILES_FINISH, - WEBIDE_MEASURE_FETCH_FILES, - WEBIDE_MARK_FETCH_FILES_START, -} from '~/performance/constants'; -import { performanceMarkAndMeasure } from '~/performance/utils'; -import { __ } from '~/locale'; -import { decorateFiles } from '../../lib/files'; -import service from '../../services'; -import * as types from '../mutation_types'; - -export const toggleTreeOpen = ({ commit }, path) => { - commit(types.TOGGLE_TREE_OPEN, path); -}; - -export const showTreeEntry = ({ commit, dispatch, state }, path) => { - const entry = state.entries[path]; - const parentPath = entry ? entry.parentPath : ''; - - if (parentPath) { - commit(types.SET_TREE_OPEN, parentPath); - - dispatch('showTreeEntry', parentPath); - } -}; - -export const handleTreeEntryAction = ({ commit, dispatch }, row) => { - if (row.type === 'tree') { - dispatch('toggleTreeOpen', row.path); - } else if (row.type === 'blob') { - if (!row.opened) { - commit(types.TOGGLE_FILE_OPEN, row.path); - } - - dispatch('setFileActive', row.path); - } - - dispatch('showTreeEntry', row.path); -}; - -export const setDirectoryData = ({ state, commit }, { projectId, branchId, treeList }) => { - const selectedTree = state.trees[`${projectId}/${branchId}`]; - - commit(types.SET_DIRECTORY_DATA, { - treePath: `${projectId}/${branchId}`, - data: treeList, - }); - commit(types.TOGGLE_LOADING, { - entry: selectedTree, - forceValue: false, - }); -}; - -export const getFiles = ({ state, commit, dispatch }, payload = {}) => { - performanceMarkAndMeasure({ mark: WEBIDE_MARK_FETCH_FILES_START }); - return new Promise((resolve, reject) => { - const { projectId, branchId, ref = branchId } = payload; - - if ( - !state.trees[`${projectId}/${branchId}`] || - (state.trees[`${projectId}/${branchId}`].tree && - state.trees[`${projectId}/${branchId}`].tree.length === 0) - ) { - const selectedProject = state.projects[projectId]; - - commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` }); - service - .getFiles(selectedProject.path_with_namespace, ref) - .then(({ data }) => { - performanceMarkAndMeasure({ - mark: WEBIDE_MARK_FETCH_FILES_FINISH, - measures: [ - { - name: WEBIDE_MEASURE_FETCH_FILES, - start: WEBIDE_MARK_FETCH_FILES_START, - }, - ], - }); - const { entries, treeList } = decorateFiles({ data }); - - commit(types.SET_ENTRIES, entries); - - // Defer setting the directory data because this triggers some intense rendering. - // The entries is all we need to load the file editor. - defer(() => dispatch('setDirectoryData', { projectId, branchId, treeList })); - - resolve(); - }) - .catch((e) => { - dispatch('setErrorMessage', { - text: __('An error occurred while loading all the files.'), - action: (actionPayload) => - dispatch('getFiles', actionPayload).then(() => dispatch('setErrorMessage', null)), - actionText: __('Please try again'), - actionPayload: { projectId, branchId }, - }); - reject(e); - }); - } else { - resolve(); - } - }); -}; - -export const restoreTree = ({ dispatch, commit, state }, path) => { - const entry = state.entries[path]; - - commit(types.RESTORE_TREE, path); - - if (entry.parentPath) { - dispatch('restoreTree', entry.parentPath); - } -}; diff --git a/app/assets/javascripts/ide/stores/extend.js b/app/assets/javascripts/ide/stores/extend.js deleted file mode 100644 index b2777ec89ff..00000000000 --- a/app/assets/javascripts/ide/stores/extend.js +++ /dev/null @@ -1,14 +0,0 @@ -import terminal from './plugins/terminal'; -import terminalSync from './plugins/terminal_sync'; - -const plugins = () => [ - terminal, - ...(gon.features && gon.features.buildServiceProxy ? [terminalSync] : []), -]; - -export default (store, el) => { - // plugins is actually an array of plugin factories, so we have to create first then call - plugins().forEach((plugin) => plugin(el)(store)); - - return store; -}; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js deleted file mode 100644 index 0f4dbb56e04..00000000000 --- a/app/assets/javascripts/ide/stores/getters.js +++ /dev/null @@ -1,262 +0,0 @@ -import Api from '~/api'; -import { addNumericSuffix } from '~/ide/utils'; -import { - leftSidebarViews, - DEFAULT_PERMISSIONS, - PERMISSION_READ_MR, - PERMISSION_CREATE_MR, - PERMISSION_PUSH_CODE, - PUSH_RULE_REJECT_UNSIGNED_COMMITS, -} from '../constants'; -import { - MSG_CANNOT_PUSH_CODE, - MSG_CANNOT_PUSH_CODE_SHOULD_FORK, - MSG_CANNOT_PUSH_CODE_GO_TO_FORK, - MSG_CANNOT_PUSH_UNSIGNED, - MSG_CANNOT_PUSH_UNSIGNED_SHORT, - MSG_FORK, - MSG_GO_TO_FORK, -} from '../messages'; -import { getChangesCountForFiles, filePathMatches } from './utils'; - -const getCannotPushCodeViewModel = (state) => { - const { ide_path: idePath, fork_path: forkPath } = state.links.forkInfo || {}; - - if (idePath) { - return { - message: MSG_CANNOT_PUSH_CODE_GO_TO_FORK, - action: { - href: idePath, - text: MSG_GO_TO_FORK, - }, - }; - } - if (forkPath) { - return { - message: MSG_CANNOT_PUSH_CODE_SHOULD_FORK, - action: { - href: forkPath, - isForm: true, - text: MSG_FORK, - }, - }; - } - - return { - message: MSG_CANNOT_PUSH_CODE, - }; -}; - -export const activeFile = (state) => state.openFiles.find((file) => file.active) || null; - -export const addedFiles = (state) => state.changedFiles.filter((f) => f.tempFile); - -export const modifiedFiles = (state) => state.changedFiles.filter((f) => !f.tempFile); - -export const projectsWithTrees = (state) => - Object.keys(state.projects).map((projectId) => { - const project = state.projects[projectId]; - - return { - ...project, - branches: Object.keys(project.branches).map((branchId) => { - const branch = project.branches[branchId]; - - return { - ...branch, - tree: state.trees[branch.treeId], - }; - }), - }; - }); - -export const currentMergeRequest = (state) => { - if ( - state.projects[state.currentProjectId] && - state.projects[state.currentProjectId].mergeRequests - ) { - return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId]; - } - return null; -}; - -export const findProject = (state) => (projectId) => state.projects[projectId]; - -export const currentProject = (state, getters) => getters.findProject(state.currentProjectId); - -export const emptyRepo = (state) => - state.projects[state.currentProjectId] && state.projects[state.currentProjectId].empty_repo; - -export const currentTree = (state) => - state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; - -export const hasMergeRequest = (state) => Boolean(state.currentMergeRequestId); - -export const allBlobs = (state) => - Object.keys(state.entries) - .reduce((acc, key) => { - const entry = state.entries[key]; - - if (entry.type === 'blob') { - acc.push(entry); - } - - return acc; - }, []) - .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); - -export const getChangedFile = (state) => (path) => state.changedFiles.find((f) => f.path === path); -export const getStagedFile = (state) => (path) => state.stagedFiles.find((f) => f.path === path); -export const getOpenFile = (state) => (path) => state.openFiles.find((f) => f.path === path); - -export const lastOpenedFile = (state) => - [...state.changedFiles, ...state.stagedFiles].sort((a, b) => b.lastOpenedAt - a.lastOpenedAt)[0]; - -export const isEditModeActive = (state) => state.currentActivityView === leftSidebarViews.edit.name; -export const isCommitModeActive = (state) => - state.currentActivityView === leftSidebarViews.commit.name; -export const isReviewModeActive = (state) => - state.currentActivityView === leftSidebarViews.review.name; - -export const someUncommittedChanges = (state) => - Boolean(state.changedFiles.length || state.stagedFiles.length); - -export const getChangesInFolder = (state) => (path) => { - const changedFilesCount = state.changedFiles.filter((f) => filePathMatches(f.path, path)).length; - const stagedFilesCount = state.stagedFiles.filter( - (f) => filePathMatches(f.path, path) && !getChangedFile(state)(f.path), - ).length; - - return changedFilesCount + stagedFilesCount; -}; - -export const getUnstagedFilesCountForPath = (state) => (path) => - getChangesCountForFiles(state.changedFiles, path); - -export const getStagedFilesCountForPath = (state) => (path) => - getChangesCountForFiles(state.stagedFiles, path); - -export const lastCommit = (state, getters) => { - const branch = getters.currentProject && getters.currentBranch; - - return branch ? branch.commit : null; -}; - -export const findBranch = (state, getters) => (projectId, branchId) => { - const project = getters.findProject(projectId); - - return project && project.branches[branchId]; -}; - -export const currentBranch = (state, getters) => - getters.findBranch(state.currentProjectId, state.currentBranchId); - -export const branchName = (_state, getters) => getters.currentBranch && getters.currentBranch.name; - -export const isOnDefaultBranch = (_state, getters) => - getters.currentProject && getters.currentProject.default_branch === getters.branchName; - -export const canPushToBranch = (_state, getters) => { - return Boolean(getters.currentBranch ? getters.currentBranch.can_push : getters.canPushCode); -}; - -export const isFileDeletedAndReadded = (state, getters) => (path) => { - const stagedFile = getters.getStagedFile(path); - const file = state.entries[path]; - return Boolean(stagedFile && stagedFile.deleted && file.tempFile); -}; - -// checks if any diff exists in the staged or unstaged changes for this path -export const getDiffInfo = (state, getters) => (path) => { - const stagedFile = getters.getStagedFile(path); - const file = state.entries[path]; - const renamed = file.prevPath ? file.path !== file.prevPath : false; - const deletedAndReadded = getters.isFileDeletedAndReadded(path); - const deleted = deletedAndReadded ? false : file.deleted; - const tempFile = deletedAndReadded ? false : file.tempFile; - const changed = file.content !== (deletedAndReadded ? stagedFile.raw : file.raw); - - return { - exists: changed || renamed || deleted || tempFile, - changed, - renamed, - deleted, - tempFile, - }; -}; - -export const findProjectPermissions = (state, getters) => (projectId) => - getters.findProject(projectId)?.userPermissions || DEFAULT_PERMISSIONS; - -export const findPushRules = (state, getters) => (projectId) => - getters.findProject(projectId)?.pushRules || {}; - -export const canReadMergeRequests = (state, getters) => - Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_READ_MR]); - -export const canCreateMergeRequests = (state, getters) => - Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_CREATE_MR]); - -/** - * Returns an object with `isAllowed` and `message` based on why the user cant push code - */ -export const canPushCodeStatus = (state, getters) => { - const canPushCode = getters.findProjectPermissions(state.currentProjectId)[PERMISSION_PUSH_CODE]; - const rejectUnsignedCommits = getters.findPushRules(state.currentProjectId)[ - PUSH_RULE_REJECT_UNSIGNED_COMMITS - ]; - - if (window.gon?.features?.rejectUnsignedCommitsByGitlab && rejectUnsignedCommits) { - return { - isAllowed: false, - message: MSG_CANNOT_PUSH_UNSIGNED, - messageShort: MSG_CANNOT_PUSH_UNSIGNED_SHORT, - }; - } - if (!canPushCode) { - return { - isAllowed: false, - messageShort: MSG_CANNOT_PUSH_CODE, - ...getCannotPushCodeViewModel(state), - }; - } - - return { - isAllowed: true, - message: '', - messageShort: '', - }; -}; - -export const canPushCode = (state, getters) => getters.canPushCodeStatus.isAllowed; - -export const entryExists = (state) => (path) => - Boolean(state.entries[path] && !state.entries[path].deleted); - -export const getAvailableFileName = (state, getters) => (path) => { - let newPath = path; - - while (getters.entryExists(newPath)) { - newPath = addNumericSuffix(newPath); - } - - return newPath; -}; - -export const getUrlForPath = (state) => (path) => - `/project/${state.currentProjectId}/tree/${state.currentBranchId}/-/${path}/`; - -export const getJsonSchemaForPath = (state, getters) => (path) => { - const [namespace, ...project] = state.currentProjectId.split('/'); - return { - uri: - // eslint-disable-next-line no-restricted-globals - location.origin + - Api.buildUrl(Api.projectFileSchemaPath) - .replace(':namespace_path', namespace) - .replace(':project_path', project.join('/')) - .replace(':ref', getters.currentBranch?.commit.id || state.currentBranchId) - .replace(':filename', path), - fileMatch: [`*${path}`], - }; -}; diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js deleted file mode 100644 index 54ae4b5aa91..00000000000 --- a/app/assets/javascripts/ide/stores/index.js +++ /dev/null @@ -1,43 +0,0 @@ -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import * as actions from './actions'; -import * as getters from './getters'; -import branches from './modules/branches'; -import commitModule from './modules/commit'; -import editorModule from './modules/editor'; -import { setupFileEditorsSync } from './modules/editor/setup'; -import fileTemplates from './modules/file_templates'; -import mergeRequests from './modules/merge_requests'; -import paneModule from './modules/pane'; -import pipelines from './modules/pipelines'; -import routerModule from './modules/router'; -import mutations from './mutations'; -import state from './state'; - -Vue.use(Vuex); - -export const createStoreOptions = () => ({ - state: state(), - actions, - mutations, - getters, - modules: { - commit: commitModule, - pipelines, - mergeRequests, - branches, - fileTemplates: fileTemplates(), - rightPane: paneModule(), - router: routerModule, - editor: editorModule, - }, -}); - -export const createStore = () => { - const store = new Vuex.Store(createStoreOptions()); - - setupFileEditorsSync(store); - - return store; -}; diff --git a/app/assets/javascripts/ide/stores/modules/branches/actions.js b/app/assets/javascripts/ide/stores/modules/branches/actions.js deleted file mode 100644 index 57a63749d7c..00000000000 --- a/app/assets/javascripts/ide/stores/modules/branches/actions.js +++ /dev/null @@ -1,34 +0,0 @@ -import Api from '~/api'; -import { __ } from '~/locale'; -import * as types from './mutation_types'; - -export const requestBranches = ({ commit }) => commit(types.REQUEST_BRANCHES); -export const receiveBranchesError = ({ commit, dispatch }, { search }) => { - dispatch( - 'setErrorMessage', - { - text: __('Error loading branches.'), - action: (payload) => - dispatch('fetchBranches', payload).then(() => - dispatch('setErrorMessage', null, { root: true }), - ), - actionText: __('Please try again'), - actionPayload: { search }, - }, - { root: true }, - ); - commit(types.RECEIVE_BRANCHES_ERROR); -}; -export const receiveBranchesSuccess = ({ commit }, data) => - commit(types.RECEIVE_BRANCHES_SUCCESS, data); - -export const fetchBranches = ({ dispatch, rootGetters }, { search = '' }) => { - dispatch('requestBranches'); - dispatch('resetBranches'); - - return Api.branches(rootGetters.currentProject.id, search, { sort: 'updated_desc' }) - .then(({ data }) => dispatch('receiveBranchesSuccess', data)) - .catch(() => dispatch('receiveBranchesError', { search })); -}; - -export const resetBranches = ({ commit }) => commit(types.RESET_BRANCHES); diff --git a/app/assets/javascripts/ide/stores/modules/branches/index.js b/app/assets/javascripts/ide/stores/modules/branches/index.js deleted file mode 100644 index 4b7d6d2998b..00000000000 --- a/app/assets/javascripts/ide/stores/modules/branches/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as actions from './actions'; -import mutations from './mutations'; -import state from './state'; - -export default { - namespaced: true, - state, - actions, - mutations, -}; diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js b/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js deleted file mode 100644 index 2272f7b9531..00000000000 --- a/app/assets/javascripts/ide/stores/modules/branches/mutation_types.js +++ /dev/null @@ -1,5 +0,0 @@ -export const REQUEST_BRANCHES = 'REQUEST_BRANCHES'; -export const RECEIVE_BRANCHES_ERROR = 'RECEIVE_BRANCHES_ERROR'; -export const RECEIVE_BRANCHES_SUCCESS = 'RECEIVE_BRANCHES_SUCCESS'; - -export const RESET_BRANCHES = 'RESET_BRANCHES'; diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutations.js b/app/assets/javascripts/ide/stores/modules/branches/mutations.js deleted file mode 100644 index 3883e1cc905..00000000000 --- a/app/assets/javascripts/ide/stores/modules/branches/mutations.js +++ /dev/null @@ -1,20 +0,0 @@ -import * as types from './mutation_types'; - -export default { - [types.REQUEST_BRANCHES](state) { - state.isLoading = true; - }, - [types.RECEIVE_BRANCHES_ERROR](state) { - state.isLoading = false; - }, - [types.RECEIVE_BRANCHES_SUCCESS](state, data) { - state.isLoading = false; - state.branches = data.map((branch) => ({ - name: branch.name, - committedDate: branch.commit.committed_date, - })); - }, - [types.RESET_BRANCHES](state) { - state.branches = []; - }, -}; diff --git a/app/assets/javascripts/ide/stores/modules/branches/state.js b/app/assets/javascripts/ide/stores/modules/branches/state.js deleted file mode 100644 index 89bf220c45f..00000000000 --- a/app/assets/javascripts/ide/stores/modules/branches/state.js +++ /dev/null @@ -1,4 +0,0 @@ -export default () => ({ - isLoading: false, - branches: [], -}); diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js deleted file mode 100644 index 79a8ccf2285..00000000000 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ /dev/null @@ -1,236 +0,0 @@ -import { createAlert } from '~/alert'; -import { addNumericSuffix } from '~/ide/utils'; -import { sprintf, __ } from '~/locale'; -import { leftSidebarViews } from '../../../constants'; -import eventHub from '../../../eventhub'; -import { parseCommitError } from '../../../lib/errors'; -import service from '../../../services'; -import * as rootTypes from '../../mutation_types'; -import { createCommitPayload, createNewMergeRequestUrl } from '../../utils'; -import { COMMIT_TO_CURRENT_BRANCH } from './constants'; -import * as types from './mutation_types'; - -export const updateCommitMessage = ({ commit }, message) => { - commit(types.UPDATE_COMMIT_MESSAGE, message); -}; - -export const discardDraft = ({ commit }) => { - commit(types.UPDATE_COMMIT_MESSAGE, ''); -}; - -export const updateCommitAction = ({ commit }, commitAction) => { - commit(types.UPDATE_COMMIT_ACTION, { commitAction }); -}; - -export const toggleShouldCreateMR = ({ commit }) => { - commit(types.TOGGLE_SHOULD_CREATE_MR); -}; - -export const updateBranchName = ({ commit }, branchName) => { - commit(types.UPDATE_NEW_BRANCH_NAME, branchName); -}; - -export const addSuffixToBranchName = ({ commit, state }) => { - const newBranchName = addNumericSuffix(state.newBranchName, true); - - commit(types.UPDATE_NEW_BRANCH_NAME, newBranchName); -}; - -export const setLastCommitMessage = ({ commit, rootGetters }, data) => { - const { currentProject } = rootGetters; - const commitStats = data.stats - ? sprintf(__('with %{additions} additions, %{deletions} deletions.'), { - additions: data.stats.additions, - deletions: data.stats.deletions, - }) - : ''; - const commitMsg = sprintf( - __('Your changes have been committed. Commit %{commitId} %{commitStats}'), - { - commitId: `${data.short_id}`, - commitStats, - }, - false, - ); - - commit(rootTypes.SET_LAST_COMMIT_MSG, commitMsg, { root: true }); -}; - -export const updateFilesAfterCommit = ({ commit, dispatch, rootState, rootGetters }, { data }) => { - const selectedProject = rootGetters.currentProject; - const lastCommit = { - commit_path: `${selectedProject.web_url}/-/commit/${data.id}`, - commit: { - id: data.id, - message: data.message, - authored_date: data.committed_date, - author_name: data.committer_name, - }, - }; - - commit( - rootTypes.SET_BRANCH_WORKING_REFERENCE, - { - projectId: rootState.currentProjectId, - branchId: rootState.currentBranchId, - reference: data.id, - }, - { root: true }, - ); - - rootState.stagedFiles.forEach((file) => { - const changedFile = rootState.changedFiles.find((f) => f.path === file.path); - - commit( - rootTypes.UPDATE_FILE_AFTER_COMMIT, - { - file, - lastCommit, - }, - { root: true }, - ); - - commit( - rootTypes.TOGGLE_FILE_CHANGED, - { - file, - changed: false, - }, - { root: true }, - ); - - dispatch('updateTempFlagForEntry', { file, tempFile: false }, { root: true }); - - eventHub.$emit(`editor.update.model.content.${file.key}`, { - content: file.content, - changed: Boolean(changedFile), - }); - }); -}; - -export const commitChanges = ({ commit, state, getters, dispatch, rootState, rootGetters }) => { - // Pull commit options out because they could change - // During some of the pre and post commit processing - const { shouldCreateMR, shouldHideNewMrOption, isCreatingNewBranch, branchName } = getters; - const newBranch = state.commitAction !== COMMIT_TO_CURRENT_BRANCH; - const stageFilesPromise = rootState.stagedFiles.length - ? Promise.resolve() - : dispatch('stageAllChanges', null, { root: true }); - - commit(types.CLEAR_ERROR); - commit(types.UPDATE_LOADING, true); - - return stageFilesPromise - .then(() => { - const payload = createCommitPayload({ - branch: branchName, - newBranch, - getters, - state, - rootState, - rootGetters, - }); - - return service.commit(rootState.currentProjectId, payload); - }) - .catch((e) => { - commit(types.UPDATE_LOADING, false); - commit(types.SET_ERROR, parseCommitError(e)); - - throw e; - }) - .then(({ data }) => { - commit(types.UPDATE_LOADING, false); - - if (!data.short_id) { - createAlert({ - message: data.message, - fadeTransition: false, - addBodyClass: true, - }); - return null; - } - - if (!data.parent_ids.length) { - commit( - rootTypes.TOGGLE_EMPTY_STATE, - { - projectPath: rootState.currentProjectId, - value: false, - }, - { root: true }, - ); - } - - dispatch('setLastCommitMessage', data); - dispatch('updateCommitMessage', ''); - return dispatch('updateFilesAfterCommit', { - data, - branch: branchName, - }) - .then(() => { - commit(rootTypes.CLEAR_STAGED_CHANGES, null, { root: true }); - - setTimeout(() => { - commit(rootTypes.SET_LAST_COMMIT_MSG, '', { root: true }); - }, 5000); - - if (shouldCreateMR && !shouldHideNewMrOption) { - const { currentProject } = rootGetters; - const targetBranch = isCreatingNewBranch - ? rootState.currentBranchId - : currentProject.default_branch; - - dispatch( - 'redirectToUrl', - createNewMergeRequestUrl( - currentProject.web_url, - encodeURIComponent(branchName), - encodeURIComponent(targetBranch), - ), - { root: true }, - ); - } - }) - .then(() => { - if (rootGetters.lastOpenedFile) { - dispatch( - 'openPendingTab', - { - file: rootGetters.lastOpenedFile, - }, - { root: true }, - ) - .then((changeViewer) => { - if (changeViewer) { - dispatch('updateViewer', 'diff', { root: true }); - } - }) - .catch((e) => { - throw e; - }); - } else { - dispatch('updateActivityBarView', leftSidebarViews.edit.name, { root: true }); - dispatch('updateViewer', 'editor', { root: true }); - } - }) - .then(() => dispatch('updateCommitAction', COMMIT_TO_CURRENT_BRANCH)) - .then(() => { - if (newBranch) { - const path = rootGetters.activeFile ? rootGetters.activeFile.path : ''; - - return dispatch( - 'router/push', - `/project/${rootState.currentProjectId}/blob/${branchName}/-/${path}`, - { root: true }, - ); - } - - return dispatch( - 'refreshLastCommitData', - { projectId: rootState.currentProjectId, branchId: branchName }, - { root: true }, - ); - }); - }); -}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/constants.js b/app/assets/javascripts/ide/stores/modules/commit/constants.js deleted file mode 100644 index 9f4299e5537..00000000000 --- a/app/assets/javascripts/ide/stores/modules/commit/constants.js +++ /dev/null @@ -1,2 +0,0 @@ -export const COMMIT_TO_CURRENT_BRANCH = '1'; -export const COMMIT_TO_NEW_BRANCH = '2'; diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js deleted file mode 100644 index 4b9176a56e7..00000000000 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ /dev/null @@ -1,64 +0,0 @@ -import { __ } from '~/locale'; -import { COMMIT_TO_NEW_BRANCH } from './constants'; - -const BRANCH_SUFFIX_COUNT = 5; -const createTranslatedTextForFiles = (files, text) => { - if (!files.length) return null; - - const filesPart = files.reduce((acc, val) => acc.concat(val.path), []).join(', '); - - return `${text} ${filesPart}`; -}; - -export const discardDraftButtonDisabled = (state) => - state.commitMessage === '' || state.submitCommitLoading; - -// Note: If changing the structure of the placeholder branch name, please also -// update #patch_branch_name in app/helpers/tree_helper.rb -export const placeholderBranchName = (state, _, rootState) => - `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr( - -BRANCH_SUFFIX_COUNT, - )}`; - -export const branchName = (state, getters, rootState) => { - if (getters.isCreatingNewBranch) { - if (state.newBranchName === '') { - return getters.placeholderBranchName; - } - - return state.newBranchName; - } - - return rootState.currentBranchId; -}; - -export const preBuiltCommitMessage = (state, _, rootState) => { - if (state.commitMessage) return state.commitMessage; - - const files = rootState.stagedFiles.length ? rootState.stagedFiles : rootState.changedFiles; - const modifiedFiles = files.filter((f) => !f.deleted); - const deletedFiles = files.filter((f) => f.deleted); - - return [ - createTranslatedTextForFiles(modifiedFiles, __('Update')), - createTranslatedTextForFiles(deletedFiles, __('Deleted')), - ] - .filter((t) => t) - .join('\n'); -}; - -export const isCreatingNewBranch = (state) => state.commitAction === COMMIT_TO_NEW_BRANCH; - -// eslint-disable-next-line max-params -export const shouldHideNewMrOption = (_state, getters, _rootState, rootGetters) => - !getters.isCreatingNewBranch && - (rootGetters.hasMergeRequest || - (!rootGetters.hasMergeRequest && rootGetters.isOnDefaultBranch)) && - rootGetters.canPushToBranch; - -// eslint-disable-next-line max-params -export const shouldDisableNewMrOption = (state, getters, rootState, rootGetters) => - !rootGetters.canCreateMergeRequests || rootGetters.emptyRepo; - -export const shouldCreateMR = (state, getters) => - state.shouldCreateMR && !getters.shouldDisableNewMrOption; diff --git a/app/assets/javascripts/ide/stores/modules/commit/index.js b/app/assets/javascripts/ide/stores/modules/commit/index.js deleted file mode 100644 index c5a39249348..00000000000 --- a/app/assets/javascripts/ide/stores/modules/commit/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; -import state from './state'; - -export default { - namespaced: true, - state, - mutations, - actions, - getters, -}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js b/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js deleted file mode 100644 index 47ec2ffbdde..00000000000 --- a/app/assets/javascripts/ide/stores/modules/commit/mutation_types.js +++ /dev/null @@ -1,8 +0,0 @@ -export const UPDATE_COMMIT_MESSAGE = 'UPDATE_COMMIT_MESSAGE'; -export const UPDATE_COMMIT_ACTION = 'UPDATE_COMMIT_ACTION'; -export const UPDATE_NEW_BRANCH_NAME = 'UPDATE_NEW_BRANCH_NAME'; -export const UPDATE_LOADING = 'UPDATE_LOADING'; -export const TOGGLE_SHOULD_CREATE_MR = 'TOGGLE_SHOULD_CREATE_MR'; - -export const CLEAR_ERROR = 'CLEAR_ERROR'; -export const SET_ERROR = 'SET_ERROR'; diff --git a/app/assets/javascripts/ide/stores/modules/commit/mutations.js b/app/assets/javascripts/ide/stores/modules/commit/mutations.js deleted file mode 100644 index c4bfad6405e..00000000000 --- a/app/assets/javascripts/ide/stores/modules/commit/mutations.js +++ /dev/null @@ -1,31 +0,0 @@ -import * as types from './mutation_types'; - -export default { - [types.UPDATE_COMMIT_MESSAGE](state, commitMessage) { - Object.assign(state, { - commitMessage, - }); - }, - [types.UPDATE_COMMIT_ACTION](state, { commitAction }) { - Object.assign(state, { commitAction }); - }, - [types.UPDATE_NEW_BRANCH_NAME](state, newBranchName) { - Object.assign(state, { newBranchName }); - }, - [types.UPDATE_LOADING](state, submitCommitLoading) { - Object.assign(state, { - submitCommitLoading, - }); - }, - [types.TOGGLE_SHOULD_CREATE_MR](state, shouldCreateMR) { - Object.assign(state, { - shouldCreateMR: shouldCreateMR === undefined ? !state.shouldCreateMR : shouldCreateMR, - }); - }, - [types.CLEAR_ERROR](state) { - state.commitError = null; - }, - [types.SET_ERROR](state, error) { - state.commitError = error; - }, -}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/state.js b/app/assets/javascripts/ide/stores/modules/commit/state.js deleted file mode 100644 index de092a569ad..00000000000 --- a/app/assets/javascripts/ide/stores/modules/commit/state.js +++ /dev/null @@ -1,8 +0,0 @@ -export default () => ({ - commitMessage: '', - commitAction: null, - newBranchName: '', - submitCommitLoading: false, - shouldCreateMR: true, - commitError: null, -}); diff --git a/app/assets/javascripts/ide/stores/modules/editor/actions.js b/app/assets/javascripts/ide/stores/modules/editor/actions.js deleted file mode 100644 index cc23a655235..00000000000 --- a/app/assets/javascripts/ide/stores/modules/editor/actions.js +++ /dev/null @@ -1,19 +0,0 @@ -import * as types from './mutation_types'; - -/** - * Action to update the current file editor info at the given `path` with the given `data` - * - * @param {} vuex - * @param {{ path: String, data: any }} payload - */ -export const updateFileEditor = ({ commit }, payload) => { - commit(types.UPDATE_FILE_EDITOR, payload); -}; - -export const removeFileEditor = ({ commit }, path) => { - commit(types.REMOVE_FILE_EDITOR, path); -}; - -export const renameFileEditor = ({ commit }, payload) => { - commit(types.RENAME_FILE_EDITOR, payload); -}; diff --git a/app/assets/javascripts/ide/stores/modules/editor/getters.js b/app/assets/javascripts/ide/stores/modules/editor/getters.js deleted file mode 100644 index 9c68e7b9f9e..00000000000 --- a/app/assets/javascripts/ide/stores/modules/editor/getters.js +++ /dev/null @@ -1,14 +0,0 @@ -import { getFileEditorOrDefault } from './utils'; - -// eslint-disable-next-line max-params -export const activeFileEditor = (state, getters, rootState, rootGetters) => { - const { activeFile } = rootGetters; - - if (!activeFile) { - return null; - } - - const { path } = rootGetters.activeFile; - - return getFileEditorOrDefault(state.fileEditors, path); -}; diff --git a/app/assets/javascripts/ide/stores/modules/editor/index.js b/app/assets/javascripts/ide/stores/modules/editor/index.js deleted file mode 100644 index b601f0e79d4..00000000000 --- a/app/assets/javascripts/ide/stores/modules/editor/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; -import state from './state'; - -export default { - namespaced: true, - actions, - state, - mutations, - getters, -}; diff --git a/app/assets/javascripts/ide/stores/modules/editor/mutation_types.js b/app/assets/javascripts/ide/stores/modules/editor/mutation_types.js deleted file mode 100644 index 89b7e9cbc76..00000000000 --- a/app/assets/javascripts/ide/stores/modules/editor/mutation_types.js +++ /dev/null @@ -1,3 +0,0 @@ -export const UPDATE_FILE_EDITOR = 'UPDATE_FILE_EDITOR'; -export const REMOVE_FILE_EDITOR = 'REMOVE_FILE_EDITOR'; -export const RENAME_FILE_EDITOR = 'RENAME_FILE_EDITOR'; diff --git a/app/assets/javascripts/ide/stores/modules/editor/mutations.js b/app/assets/javascripts/ide/stores/modules/editor/mutations.js deleted file mode 100644 index 1cbf82c0f12..00000000000 --- a/app/assets/javascripts/ide/stores/modules/editor/mutations.js +++ /dev/null @@ -1,38 +0,0 @@ -import * as types from './mutation_types'; -import { getFileEditorOrDefault } from './utils'; - -const deletePropertyAndReturnNewCopy = (source, property) => { - const fileEditorsCopy = { ...source }; - delete fileEditorsCopy[property]; - - return fileEditorsCopy; -}; - -export default { - [types.UPDATE_FILE_EDITOR](state, { path, data }) { - const editor = getFileEditorOrDefault(state.fileEditors, path); - - state.fileEditors = { - ...state.fileEditors, - [path]: Object.assign(editor, data), - }; - }, - [types.REMOVE_FILE_EDITOR](state, path) { - state.fileEditors = deletePropertyAndReturnNewCopy(state.fileEditors, path); - }, - [types.RENAME_FILE_EDITOR](state, { path, newPath }) { - const existing = state.fileEditors[path]; - - // Gracefully do nothing if fileEditor isn't found. - if (!existing) { - return; - } - - state.fileEditors = deletePropertyAndReturnNewCopy(state.fileEditors, path); - - state.fileEditors = { - ...state.fileEditors, - [newPath]: existing, - }; - }, -}; diff --git a/app/assets/javascripts/ide/stores/modules/editor/setup.js b/app/assets/javascripts/ide/stores/modules/editor/setup.js deleted file mode 100644 index 5899706e38a..00000000000 --- a/app/assets/javascripts/ide/stores/modules/editor/setup.js +++ /dev/null @@ -1,24 +0,0 @@ -import { commitActionTypes } from '~/ide/constants'; -import eventHub from '~/ide/eventhub'; - -const removeUnusedFileEditors = (store) => { - Object.keys(store.state.editor.fileEditors) - .filter((path) => !store.state.entries[path]) - .forEach((path) => store.dispatch('editor/removeFileEditor', path)); -}; - -export const setupFileEditorsSync = (store) => { - eventHub.$on('ide.files.change', ({ type, ...payload } = {}) => { - // Do nothing on file update because the file tree itself hasn't changed. - if (type === commitActionTypes.update) { - return; - } - - if (type === commitActionTypes.move) { - store.dispatch('editor/renameFileEditor', payload); - } else { - // The file tree has changed, but the specific change is not known. - removeUnusedFileEditors(store); - } - }); -}; diff --git a/app/assets/javascripts/ide/stores/modules/editor/state.js b/app/assets/javascripts/ide/stores/modules/editor/state.js deleted file mode 100644 index 484aeec5cc3..00000000000 --- a/app/assets/javascripts/ide/stores/modules/editor/state.js +++ /dev/null @@ -1,8 +0,0 @@ -export default () => ({ - // Object which represents a dictionary of filePath to editor specific properties, including: - // - fileLanguage - // - editorRow - // - editorCol - // - viewMode - fileEditors: {}, -}); diff --git a/app/assets/javascripts/ide/stores/modules/editor/utils.js b/app/assets/javascripts/ide/stores/modules/editor/utils.js deleted file mode 100644 index bef21d04b2b..00000000000 --- a/app/assets/javascripts/ide/stores/modules/editor/utils.js +++ /dev/null @@ -1,11 +0,0 @@ -import { FILE_VIEW_MODE_EDITOR } from '../../../constants'; - -export const createDefaultFileEditor = () => ({ - editorRow: 1, - editorColumn: 1, - fileLanguage: '', - viewMode: FILE_VIEW_MODE_EDITOR, -}); - -export const getFileEditorOrDefault = (fileEditors, path) => - fileEditors[path] || createDefaultFileEditor(); diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js deleted file mode 100644 index bc96ec267b5..00000000000 --- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js +++ /dev/null @@ -1,119 +0,0 @@ -import Api from '~/api'; -import { normalizeHeaders } from '~/lib/utils/common_utils'; -import { __ } from '~/locale'; -import eventHub from '../../../eventhub'; -import * as types from './mutation_types'; - -export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES); -export const receiveTemplateTypesError = ({ commit, dispatch }) => { - commit(types.RECEIVE_TEMPLATE_TYPES_ERROR); - dispatch( - 'setErrorMessage', - { - text: __('Error loading template types.'), - action: () => - dispatch('fetchTemplateTypes').then(() => - dispatch('setErrorMessage', null, { root: true }), - ), - actionText: __('Please try again'), - }, - { root: true }, - ); -}; -export const receiveTemplateTypesSuccess = ({ commit }, templates) => - commit(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, templates); - -export const fetchTemplateTypes = ({ dispatch, state, rootState }) => { - if (!Object.keys(state.selectedTemplateType).length) return Promise.reject(); - - dispatch('requestTemplateTypes'); - - const fetchPages = (page = 1, prev = []) => - Api.projectTemplates(rootState.currentProjectId, state.selectedTemplateType.key, { - page, - per_page: 100, - }) - .then(({ data, headers }) => { - const nextPage = parseInt(normalizeHeaders(headers)['X-NEXT-PAGE'], 10); - const nextData = prev.concat(data); - - dispatch('receiveTemplateTypesSuccess', nextData); - - return nextPage ? fetchPages(nextPage, nextData) : nextData; - }) - .catch(() => dispatch('receiveTemplateTypesError')); - - return fetchPages(); -}; - -export const setSelectedTemplateType = ({ commit, dispatch, rootGetters }, type) => { - commit(types.SET_SELECTED_TEMPLATE_TYPE, type); - - if (rootGetters.activeFile.prevPath === type.name) { - dispatch('discardFileChanges', rootGetters.activeFile.path, { root: true }); - } else if (rootGetters.activeFile.name !== type.name) { - dispatch( - 'renameEntry', - { - path: rootGetters.activeFile.path, - name: type.name, - }, - { root: true }, - ); - } -}; - -export const receiveTemplateError = ({ dispatch }, template) => { - dispatch( - 'setErrorMessage', - { - text: __('Error loading template.'), - action: (payload) => - dispatch('fetchTemplateTypes', payload).then(() => - dispatch('setErrorMessage', null, { root: true }), - ), - actionText: __('Please try again'), - actionPayload: template, - }, - { root: true }, - ); -}; - -export const fetchTemplate = ({ dispatch, state, rootState }, template) => { - if (template.content) { - return dispatch('setFileTemplate', template); - } - - return Api.projectTemplate( - rootState.currentProjectId, - state.selectedTemplateType.key, - template.key || template.name, - ) - .then(({ data }) => { - dispatch('setFileTemplate', data); - }) - .catch(() => dispatch('receiveTemplateError', template)); -}; - -export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) => { - dispatch( - 'changeFileContent', - { path: rootGetters.activeFile.path, content: template.content }, - { root: true }, - ); - commit(types.SET_UPDATE_SUCCESS, true); - eventHub.$emit(`editor.update.model.new.content.${rootGetters.activeFile.key}`, template.content); -}; - -export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => { - const file = rootGetters.activeFile; - - dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true }); - commit(types.SET_UPDATE_SUCCESS, false); - - eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.raw); - - if (file.prevPath) { - dispatch('discardFileChanges', file.path, { root: true }); - } -}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js deleted file mode 100644 index 5681f6cdec5..00000000000 --- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js +++ /dev/null @@ -1,26 +0,0 @@ -import { __ } from '~/locale'; -import { DEFAULT_CI_CONFIG_PATH } from '~/lib/utils/constants'; -import { leftSidebarViews } from '../../../constants'; - -export const templateTypes = () => [ - { - name: DEFAULT_CI_CONFIG_PATH, - key: 'gitlab_ci_ymls', - }, - { - name: '.gitignore', - key: 'gitignores', - }, - { - name: __('LICENSE'), - key: 'licenses', - }, - { - name: __('Dockerfile'), - key: 'dockerfiles', - }, -]; - -export const showFileTemplatesBar = (_, getters, rootState) => (name) => - getters.templateTypes.find((t) => t.name === name) && - rootState.currentActivityView === leftSidebarViews.edit.name; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/index.js b/app/assets/javascripts/ide/stores/modules/file_templates/index.js deleted file mode 100644 index 5f850b8b86a..00000000000 --- a/app/assets/javascripts/ide/stores/modules/file_templates/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; -import createState from './state'; - -export default () => ({ - namespaced: true, - actions, - state: createState(), - getters, - mutations, -}); diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutation_types.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutation_types.js deleted file mode 100644 index cf4499c0264..00000000000 --- a/app/assets/javascripts/ide/stores/modules/file_templates/mutation_types.js +++ /dev/null @@ -1,7 +0,0 @@ -export const REQUEST_TEMPLATE_TYPES = 'REQUEST_TEMPLATE_TYPES'; -export const RECEIVE_TEMPLATE_TYPES_ERROR = 'RECEIVE_TEMPLATE_TYPES_ERROR'; -export const RECEIVE_TEMPLATE_TYPES_SUCCESS = 'RECEIVE_TEMPLATE_TYPES_SUCCESS'; - -export const SET_SELECTED_TEMPLATE_TYPE = 'SET_SELECTED_TEMPLATE_TYPE'; - -export const SET_UPDATE_SUCCESS = 'SET_UPDATE_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js deleted file mode 100644 index 7fc1c9134a7..00000000000 --- a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js +++ /dev/null @@ -1,22 +0,0 @@ -import * as types from './mutation_types'; - -export default { - [types.REQUEST_TEMPLATE_TYPES](state) { - state.isLoading = true; - state.templates = []; - }, - [types.RECEIVE_TEMPLATE_TYPES_ERROR](state) { - state.isLoading = false; - }, - [types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, templates) { - state.isLoading = false; - state.templates = templates; - }, - [types.SET_SELECTED_TEMPLATE_TYPE](state, type) { - state.selectedTemplateType = type; - state.templates = []; - }, - [types.SET_UPDATE_SUCCESS](state, success) { - state.updateSuccess = success; - }, -}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/state.js b/app/assets/javascripts/ide/stores/modules/file_templates/state.js deleted file mode 100644 index bd4b7d7bc52..00000000000 --- a/app/assets/javascripts/ide/stores/modules/file_templates/state.js +++ /dev/null @@ -1,6 +0,0 @@ -export default () => ({ - isLoading: false, - templates: [], - selectedTemplateType: {}, - updateSuccess: false, -}); diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js b/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js deleted file mode 100644 index 3408245b245..00000000000 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/actions.js +++ /dev/null @@ -1,43 +0,0 @@ -import Api from '~/api'; -import { __ } from '~/locale'; -import { scopes } from './constants'; -import * as types from './mutation_types'; - -export const requestMergeRequests = ({ commit }) => commit(types.REQUEST_MERGE_REQUESTS); -export const receiveMergeRequestsError = ({ commit, dispatch }, { type, search }) => { - dispatch( - 'setErrorMessage', - { - text: __('Error loading merge requests.'), - action: (payload) => - dispatch('fetchMergeRequests', payload).then(() => - dispatch('setErrorMessage', null, { root: true }), - ), - actionText: __('Please try again'), - actionPayload: { type, search }, - }, - { root: true }, - ); - commit(types.RECEIVE_MERGE_REQUESTS_ERROR); -}; -export const receiveMergeRequestsSuccess = ({ commit }, data) => - commit(types.RECEIVE_MERGE_REQUESTS_SUCCESS, data); - -export const fetchMergeRequests = ( - { dispatch, state: { state }, rootState: { currentProjectId } }, - { type, search = '' }, -) => { - dispatch('requestMergeRequests'); - dispatch('resetMergeRequests'); - - const scope = type && scopes[type]; - const request = scope - ? Api.mergeRequests({ scope, state, search }) - : Api.projectMergeRequest(currentProjectId, '', { state, search }); - - return request - .then(({ data }) => dispatch('receiveMergeRequestsSuccess', data)) - .catch(() => dispatch('receiveMergeRequestsError', { type, search })); -}; - -export const resetMergeRequests = ({ commit }) => commit(types.RESET_MERGE_REQUESTS); diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js b/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js deleted file mode 100644 index b5bb2c7bdf8..00000000000 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/constants.js +++ /dev/null @@ -1,4 +0,0 @@ -export const scopes = { - assigned: 'assigned-to-me', - created: 'created-by-me', -}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js b/app/assets/javascripts/ide/stores/modules/merge_requests/index.js deleted file mode 100644 index d858a855d9e..00000000000 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as actions from './actions'; -import mutations from './mutations'; -import state from './state'; - -export default { - namespaced: true, - state: state(), - actions, - mutations, -}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js deleted file mode 100644 index 0badddcbae7..00000000000 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutation_types.js +++ /dev/null @@ -1,5 +0,0 @@ -export const REQUEST_MERGE_REQUESTS = 'REQUEST_MERGE_REQUESTS'; -export const RECEIVE_MERGE_REQUESTS_ERROR = 'RECEIVE_MERGE_REQUESTS_ERROR'; -export const RECEIVE_MERGE_REQUESTS_SUCCESS = 'RECEIVE_MERGE_REQUESTS_SUCCESS'; - -export const RESET_MERGE_REQUESTS = 'RESET_MERGE_REQUESTS'; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js deleted file mode 100644 index eae64ad80c3..00000000000 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js +++ /dev/null @@ -1,26 +0,0 @@ -import * as types from './mutation_types'; - -export default { - [types.REQUEST_MERGE_REQUESTS](state) { - state.isLoading = true; - }, - [types.RECEIVE_MERGE_REQUESTS_ERROR](state) { - state.isLoading = false; - }, - [types.RECEIVE_MERGE_REQUESTS_SUCCESS](state, data) { - state.isLoading = false; - state.mergeRequests = data.map((mergeRequest) => ({ - id: mergeRequest.id, - iid: mergeRequest.iid, - title: mergeRequest.title, - projectId: mergeRequest.project_id, - projectPathWithNamespace: mergeRequest.references.full.replace( - mergeRequest.references.short, - '', - ), - })); - }, - [types.RESET_MERGE_REQUESTS](state) { - state.mergeRequests = []; - }, -}; diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js b/app/assets/javascripts/ide/stores/modules/merge_requests/state.js deleted file mode 100644 index 0a2f778c715..00000000000 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/state.js +++ /dev/null @@ -1,7 +0,0 @@ -import { STATUS_OPEN } from '~/issues/constants'; - -export default () => ({ - isLoading: false, - mergeRequests: [], - state: STATUS_OPEN, -}); diff --git a/app/assets/javascripts/ide/stores/modules/pane/actions.js b/app/assets/javascripts/ide/stores/modules/pane/actions.js deleted file mode 100644 index b7cff368fe4..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pane/actions.js +++ /dev/null @@ -1,27 +0,0 @@ -import * as types from './mutation_types'; - -export const toggleOpen = ({ dispatch, state }) => { - if (state.isOpen) { - dispatch('close'); - } else { - dispatch('open'); - } -}; - -export const open = ({ state, commit }, view) => { - commit(types.SET_OPEN, true); - - if (view && view.name !== state.currentView) { - const { name, keepAlive } = view; - - commit(types.SET_CURRENT_VIEW, name); - - if (keepAlive) { - commit(types.KEEP_ALIVE_VIEW, name); - } - } -}; - -export const close = ({ commit }) => { - commit(types.SET_OPEN, false); -}; diff --git a/app/assets/javascripts/ide/stores/modules/pane/getters.js b/app/assets/javascripts/ide/stores/modules/pane/getters.js deleted file mode 100644 index 66d23c8ebdc..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pane/getters.js +++ /dev/null @@ -1,2 +0,0 @@ -export const isAliveView = (state) => (view) => - state.keepAliveViews[view] || (state.isOpen && state.currentView === view); diff --git a/app/assets/javascripts/ide/stores/modules/pane/index.js b/app/assets/javascripts/ide/stores/modules/pane/index.js deleted file mode 100644 index 5f61cb732c8..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pane/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; -import state from './state'; - -export default () => ({ - namespaced: true, - state: state(), - actions, - getters, - mutations, -}); diff --git a/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js deleted file mode 100644 index abdebc4d913..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js +++ /dev/null @@ -1,3 +0,0 @@ -export const SET_OPEN = 'SET_OPEN'; -export const SET_CURRENT_VIEW = 'SET_CURRENT_VIEW'; -export const KEEP_ALIVE_VIEW = 'KEEP_ALIVE_VIEW'; diff --git a/app/assets/javascripts/ide/stores/modules/pane/mutations.js b/app/assets/javascripts/ide/stores/modules/pane/mutations.js deleted file mode 100644 index c16484b4402..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pane/mutations.js +++ /dev/null @@ -1,19 +0,0 @@ -import * as types from './mutation_types'; - -export default { - [types.SET_OPEN](state, isOpen) { - Object.assign(state, { - isOpen, - }); - }, - [types.SET_CURRENT_VIEW](state, currentView) { - Object.assign(state, { - currentView, - }); - }, - [types.KEEP_ALIVE_VIEW](state, viewName) { - Object.assign(state.keepAliveViews, { - [viewName]: true, - }); - }, -}; diff --git a/app/assets/javascripts/ide/stores/modules/pane/state.js b/app/assets/javascripts/ide/stores/modules/pane/state.js deleted file mode 100644 index 353065b5735..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pane/state.js +++ /dev/null @@ -1,5 +0,0 @@ -export default () => ({ - isOpen: false, - currentView: null, - keepAliveViews: {}, -}); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js deleted file mode 100644 index 8554d78dd6e..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ /dev/null @@ -1,158 +0,0 @@ -import Visibility from 'visibilityjs'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status'; -import Poll from '~/lib/utils/poll'; -import { __ } from '~/locale'; -import { rightSidebarViews } from '../../../constants'; -import service from '../../../services'; -import * as types from './mutation_types'; - -let eTagPoll; - -export const clearEtagPoll = () => { - eTagPoll = null; -}; -export const stopPipelinePolling = () => { - if (eTagPoll) eTagPoll.stop(); -}; -export const restartPipelinePolling = () => { - if (eTagPoll) eTagPoll.restart(); -}; -export const forcePipelineRequest = () => { - if (eTagPoll) eTagPoll.makeRequest(); -}; - -export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE); -export const receiveLatestPipelineError = ({ commit, dispatch }, err) => { - if (err.status !== HTTP_STATUS_NOT_FOUND) { - dispatch( - 'setErrorMessage', - { - text: __('An error occurred while fetching the latest pipeline.'), - action: () => - dispatch('forcePipelineRequest').then(() => - dispatch('setErrorMessage', null, { root: true }), - ), - actionText: __('Please try again'), - actionPayload: null, - }, - { root: true }, - ); - } - commit(types.RECEIVE_LASTEST_PIPELINE_ERROR); - dispatch('stopPipelinePolling'); -}; -export const receiveLatestPipelineSuccess = ({ rootGetters, commit }, { pipelines }) => { - let lastCommitPipeline = false; - - if (pipelines && pipelines.length) { - const lastCommitHash = rootGetters.lastCommit && rootGetters.lastCommit.id; - lastCommitPipeline = pipelines.find((pipeline) => pipeline.commit.id === lastCommitHash); - } - - commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, lastCommitPipeline); -}; - -export const fetchLatestPipeline = ({ commit, dispatch, rootGetters }) => { - if (eTagPoll) return; - - if (!rootGetters.lastCommit) { - commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null); - dispatch('stopPipelinePolling'); - return; - } - - dispatch('requestLatestPipeline'); - - eTagPoll = new Poll({ - resource: service, - method: 'lastCommitPipelines', - data: { getters: rootGetters }, - successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data), - errorCallback: (err) => dispatch('receiveLatestPipelineError', err), - }); - - if (!Visibility.hidden()) { - eTagPoll.makeRequest(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - dispatch('restartPipelinePolling'); - } else { - dispatch('stopPipelinePolling'); - } - }); -}; - -export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id); -export const receiveJobsError = ({ commit, dispatch }, stage) => { - dispatch( - 'setErrorMessage', - { - text: __('An error occurred while loading the pipelines jobs.'), - action: (payload) => - dispatch('fetchJobs', payload).then(() => - dispatch('setErrorMessage', null, { root: true }), - ), - actionText: __('Please try again'), - actionPayload: stage, - }, - { root: true }, - ); - commit(types.RECEIVE_JOBS_ERROR, stage.id); -}; -export const receiveJobsSuccess = ({ commit }, { id, data }) => - commit(types.RECEIVE_JOBS_SUCCESS, { id, data }); - -export const fetchJobs = ({ dispatch }, stage) => { - dispatch('requestJobs', stage.id); - - return axios - .get(stage.dropdownPath) - .then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data })) - .catch(() => dispatch('receiveJobsError', stage)); -}; - -export const toggleStageCollapsed = ({ commit }, stageId) => - commit(types.TOGGLE_STAGE_COLLAPSE, stageId); - -export const setDetailJob = ({ commit, dispatch }, job) => { - commit(types.SET_DETAIL_JOB, job); - dispatch('rightPane/open', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, { - root: true, - }); -}; - -export const requestJobLogs = ({ commit }) => commit(types.REQUEST_JOB_LOGS); -export const receiveJobLogsError = ({ commit, dispatch }) => { - dispatch( - 'setErrorMessage', - { - text: __('An error occurred while fetching the job logs.'), - action: () => - dispatch('fetchJobLogs').then(() => dispatch('setErrorMessage', null, { root: true })), - actionText: __('Please try again'), - actionPayload: null, - }, - { root: true }, - ); - commit(types.RECEIVE_JOB_LOGS_ERROR); -}; -export const receiveJobLogsSuccess = ({ commit }, data) => - commit(types.RECEIVE_JOB_LOGS_SUCCESS, data); - -export const fetchJobLogs = ({ dispatch, state }) => { - dispatch('requestJobLogs'); - - // update trace endpoint once BE compeletes trace re-naming in #340626 - return axios - .get(`${state.detailJob.path}/trace`, { params: { format: 'json' } }) - .then(({ data }) => dispatch('receiveJobLogsSuccess', data)) - .catch(() => dispatch('receiveJobLogsError')); -}; - -export const resetLatestPipeline = ({ commit }) => { - commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null); - commit(types.SET_DETAIL_JOB, null); -}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/constants.js b/app/assets/javascripts/ide/stores/modules/pipelines/constants.js deleted file mode 100644 index bb4145934ff..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pipelines/constants.js +++ /dev/null @@ -1,3 +0,0 @@ -export const states = { - failed: 'failed', -}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js b/app/assets/javascripts/ide/stores/modules/pipelines/getters.js deleted file mode 100644 index 051159a0fd5..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pipelines/getters.js +++ /dev/null @@ -1,23 +0,0 @@ -import { states } from './constants'; - -export const hasLatestPipeline = (state) => - !state.isLoadingPipeline && Boolean(state.latestPipeline); - -export const pipelineFailed = (state) => - state.latestPipeline && state.latestPipeline.details.status.text === states.failed; - -export const failedStages = (state) => - state.stages - .filter((stage) => stage.status.text.toLowerCase() === states.failed) - .map((stage) => ({ - ...stage, - jobs: stage.jobs.filter((job) => job.status.text.toLowerCase() === states.failed), - })); - -export const failedJobsCount = (state) => - state.stages.reduce( - (acc, stage) => acc + stage.jobs.filter((j) => j.status.text === states.failed).length, - 0, - ); - -export const jobsCount = (state) => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/index.js b/app/assets/javascripts/ide/stores/modules/pipelines/index.js deleted file mode 100644 index b48f63887ca..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pipelines/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; -import state from './state'; - -export default { - namespaced: true, - state: state(), - actions, - mutations, - getters, -}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js deleted file mode 100644 index fea3055e0fe..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutation_types.js +++ /dev/null @@ -1,15 +0,0 @@ -export const REQUEST_LATEST_PIPELINE = 'REQUEST_LATEST_PIPELINE'; -export const RECEIVE_LASTEST_PIPELINE_ERROR = 'RECEIVE_LASTEST_PIPELINE_ERROR'; -export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCESS'; - -export const REQUEST_JOBS = 'REQUEST_JOBS'; -export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR'; -export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; - -export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE'; - -export const SET_DETAIL_JOB = 'SET_DETAIL_JOB'; - -export const REQUEST_JOB_LOGS = 'REQUEST_JOB_LOGS'; -export const RECEIVE_JOB_LOGS_ERROR = 'RECEIVE_JOB_LOGS_ERROR'; -export const RECEIVE_JOB_LOGS_SUCCESS = 'RECEIVE_JOB_LOGS_SUCCESS'; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js deleted file mode 100644 index 09006df7e94..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js +++ /dev/null @@ -1,79 +0,0 @@ -import * as types from './mutation_types'; -import { normalizeJob } from './utils'; - -export default { - [types.REQUEST_LATEST_PIPELINE](state) { - state.isLoadingPipeline = true; - }, - [types.RECEIVE_LASTEST_PIPELINE_ERROR](state) { - state.isLoadingPipeline = false; - }, - [types.RECEIVE_LASTEST_PIPELINE_SUCCESS](state, pipeline) { - state.isLoadingPipeline = false; - state.hasLoadedPipeline = true; - - if (pipeline) { - state.latestPipeline = { - id: pipeline.id, - path: pipeline.path, - commit: pipeline.commit, - details: { - status: pipeline.details.status, - }, - yamlError: pipeline.yaml_errors, - }; - state.stages = pipeline.details.stages.map((stage, i) => { - const foundStage = state.stages.find((s) => s.id === i); - return { - id: i, - dropdownPath: stage.dropdown_path, - name: stage.name, - status: stage.status, - isCollapsed: foundStage ? foundStage.isCollapsed : false, - isLoading: foundStage ? foundStage.isLoading : false, - jobs: foundStage ? foundStage.jobs : [], - }; - }); - } else { - state.latestPipeline = null; - } - }, - [types.REQUEST_JOBS](state, id) { - state.stages = state.stages.map((stage) => ({ - ...stage, - isLoading: stage.id === id ? true : stage.isLoading, - })); - }, - [types.RECEIVE_JOBS_ERROR](state, id) { - state.stages = state.stages.map((stage) => ({ - ...stage, - isLoading: stage.id === id ? false : stage.isLoading, - })); - }, - [types.RECEIVE_JOBS_SUCCESS](state, { id, data }) { - state.stages = state.stages.map((stage) => ({ - ...stage, - isLoading: stage.id === id ? false : stage.isLoading, - jobs: stage.id === id ? data.latest_statuses.map(normalizeJob) : stage.jobs, - })); - }, - [types.TOGGLE_STAGE_COLLAPSE](state, id) { - state.stages = state.stages.map((stage) => ({ - ...stage, - isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed, - })); - }, - [types.SET_DETAIL_JOB](state, job) { - state.detailJob = { ...job }; - }, - [types.REQUEST_JOB_LOGS](state) { - state.detailJob.isLoading = true; - }, - [types.RECEIVE_JOB_LOGS_ERROR](state) { - state.detailJob.isLoading = false; - }, - [types.RECEIVE_JOB_LOGS_SUCCESS](state, data) { - state.detailJob.isLoading = false; - state.detailJob.output = data.html; - }, -}; diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/state.js b/app/assets/javascripts/ide/stores/modules/pipelines/state.js deleted file mode 100644 index 8dfa0ec491f..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pipelines/state.js +++ /dev/null @@ -1,8 +0,0 @@ -export default () => ({ - isLoadingPipeline: true, - hasLoadedPipeline: false, - isLoadingJobs: false, - latestPipeline: null, - stages: [], - detailJob: null, -}); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js b/app/assets/javascripts/ide/stores/modules/pipelines/utils.js deleted file mode 100644 index ded00196ab7..00000000000 --- a/app/assets/javascripts/ide/stores/modules/pipelines/utils.js +++ /dev/null @@ -1,10 +0,0 @@ -export const normalizeJob = (job) => ({ - id: job.id, - name: job.name, - status: job.status, - path: job.build_path, - rawPath: `${job.build_path}/raw`, - started: job.started, - output: '', - isLoading: false, -}); diff --git a/app/assets/javascripts/ide/stores/modules/router/actions.js b/app/assets/javascripts/ide/stores/modules/router/actions.js deleted file mode 100644 index 321ac57817f..00000000000 --- a/app/assets/javascripts/ide/stores/modules/router/actions.js +++ /dev/null @@ -1,5 +0,0 @@ -import * as types from './mutation_types'; - -export const push = ({ commit }, fullPath) => { - commit(types.PUSH, fullPath); -}; diff --git a/app/assets/javascripts/ide/stores/modules/router/index.js b/app/assets/javascripts/ide/stores/modules/router/index.js deleted file mode 100644 index 1d5af1d4fe5..00000000000 --- a/app/assets/javascripts/ide/stores/modules/router/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as actions from './actions'; -import mutations from './mutations'; -import state from './state'; - -export default { - namespaced: true, - state, - mutations, - actions, -}; diff --git a/app/assets/javascripts/ide/stores/modules/router/mutation_types.js b/app/assets/javascripts/ide/stores/modules/router/mutation_types.js deleted file mode 100644 index 8f5f949bd5f..00000000000 --- a/app/assets/javascripts/ide/stores/modules/router/mutation_types.js +++ /dev/null @@ -1 +0,0 @@ -export const PUSH = 'PUSH'; diff --git a/app/assets/javascripts/ide/stores/modules/router/mutations.js b/app/assets/javascripts/ide/stores/modules/router/mutations.js deleted file mode 100644 index 471cace314c..00000000000 --- a/app/assets/javascripts/ide/stores/modules/router/mutations.js +++ /dev/null @@ -1,7 +0,0 @@ -import * as types from './mutation_types'; - -export default { - [types.PUSH](state, fullPath) { - state.fullPath = fullPath; - }, -}; diff --git a/app/assets/javascripts/ide/stores/modules/router/state.js b/app/assets/javascripts/ide/stores/modules/router/state.js deleted file mode 100644 index abb6c5239e4..00000000000 --- a/app/assets/javascripts/ide/stores/modules/router/state.js +++ /dev/null @@ -1,3 +0,0 @@ -export default () => ({ - fullPath: '', -}); diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js deleted file mode 100644 index c4198a7427f..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/actions/checks.js +++ /dev/null @@ -1,98 +0,0 @@ -import Api from '~/api'; -import { HTTP_STATUS_FORBIDDEN, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status'; -import * as terminalService from '../../../../services/terminals'; -import { CHECK_CONFIG, CHECK_RUNNERS, RETRY_RUNNERS_INTERVAL } from '../constants'; -import * as messages from '../messages'; -import * as types from '../mutation_types'; - -export const requestConfigCheck = ({ commit }) => { - commit(types.REQUEST_CHECK, CHECK_CONFIG); -}; - -export const receiveConfigCheckSuccess = ({ commit }) => { - commit(types.SET_VISIBLE, true); - commit(types.RECEIVE_CHECK_SUCCESS, CHECK_CONFIG); -}; - -export const receiveConfigCheckError = ({ commit, state }, e) => { - const { status } = e.response; - const { paths } = state; - - const isVisible = status !== HTTP_STATUS_FORBIDDEN && status !== HTTP_STATUS_NOT_FOUND; - commit(types.SET_VISIBLE, isVisible); - - const message = messages.configCheckError(status, paths.webTerminalConfigHelpPath); - commit(types.RECEIVE_CHECK_ERROR, { type: CHECK_CONFIG, message }); -}; - -export const fetchConfigCheck = ({ dispatch, rootState, rootGetters }) => { - dispatch('requestConfigCheck'); - - const { currentBranchId } = rootState; - const { currentProject } = rootGetters; - - terminalService - .checkConfig(currentProject.path_with_namespace, currentBranchId) - .then(() => { - dispatch('receiveConfigCheckSuccess'); - }) - .catch((e) => { - dispatch('receiveConfigCheckError', e); - }); -}; - -export const requestRunnersCheck = ({ commit }) => { - commit(types.REQUEST_CHECK, CHECK_RUNNERS); -}; - -export const receiveRunnersCheckSuccess = ({ commit, dispatch, state }, data) => { - if (data.length) { - commit(types.RECEIVE_CHECK_SUCCESS, CHECK_RUNNERS); - } else { - const { paths } = state; - - commit(types.RECEIVE_CHECK_ERROR, { - type: CHECK_RUNNERS, - message: messages.runnersCheckEmpty(paths.webTerminalRunnersHelpPath), - }); - - dispatch('retryRunnersCheck'); - } -}; - -export const receiveRunnersCheckError = ({ commit }) => { - commit(types.RECEIVE_CHECK_ERROR, { - type: CHECK_RUNNERS, - message: messages.UNEXPECTED_ERROR_RUNNERS, - }); -}; - -export const retryRunnersCheck = ({ dispatch, state }) => { - // if the overall check has failed, don't worry about retrying - const check = state.checks[CHECK_CONFIG]; - if (!check.isLoading && !check.isValid) { - return; - } - - setTimeout(() => { - dispatch('fetchRunnersCheck', { background: true }); - }, RETRY_RUNNERS_INTERVAL); -}; - -export const fetchRunnersCheck = ({ dispatch, rootGetters }, options = {}) => { - const { background = false } = options; - - if (!background) { - dispatch('requestRunnersCheck'); - } - - const { currentProject } = rootGetters; - - Api.projectRunners(currentProject.id, { params: { scope: 'active' } }) - .then(({ data }) => { - dispatch('receiveRunnersCheckSuccess', data); - }) - .catch((e) => { - dispatch('receiveRunnersCheckError', e); - }); -}; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/index.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/index.js deleted file mode 100644 index 5c13b5d74f2..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/actions/index.js +++ /dev/null @@ -1,4 +0,0 @@ -export * from './setup'; -export * from './checks'; -export * from './session_controls'; -export * from './session_status'; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js deleted file mode 100644 index 411ff0beaba..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js +++ /dev/null @@ -1,118 +0,0 @@ -import { createAlert } from '~/alert'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status'; -import * as terminalService from '../../../../services/terminals'; -import { STARTING, STOPPING, STOPPED } from '../constants'; -import * as messages from '../messages'; -import * as types from '../mutation_types'; - -export const requestStartSession = ({ commit }) => { - commit(types.SET_SESSION_STATUS, STARTING); -}; - -export const receiveStartSessionSuccess = ({ commit, dispatch }, data) => { - commit(types.SET_SESSION, { - id: data.id, - status: data.status, - showPath: data.show_path, - cancelPath: data.cancel_path, - retryPath: data.retry_path, - terminalPath: data.terminal_path, - proxyWebsocketPath: data.proxy_websocket_path, - services: data.services, - }); - - dispatch('pollSessionStatus'); -}; - -export const receiveStartSessionError = ({ dispatch }) => { - createAlert({ message: messages.UNEXPECTED_ERROR_STARTING }); - dispatch('killSession'); -}; - -export const startSession = ({ state, dispatch, rootGetters, rootState }) => { - if (state.session && state.session.status === STARTING) { - return; - } - - const { currentProject } = rootGetters; - const { currentBranchId } = rootState; - - dispatch('requestStartSession'); - - terminalService - .create(currentProject.path_with_namespace, currentBranchId) - .then(({ data }) => { - dispatch('receiveStartSessionSuccess', data); - }) - .catch((error) => { - dispatch('receiveStartSessionError', error); - }); -}; - -export const requestStopSession = ({ commit }) => { - commit(types.SET_SESSION_STATUS, STOPPING); -}; - -export const receiveStopSessionSuccess = ({ dispatch }) => { - dispatch('killSession'); -}; - -export const receiveStopSessionError = ({ dispatch }) => { - createAlert({ message: messages.UNEXPECTED_ERROR_STOPPING }); - dispatch('killSession'); -}; - -export const stopSession = ({ state, dispatch }) => { - const { cancelPath } = state.session; - - dispatch('requestStopSession'); - - axios - .post(cancelPath) - .then(() => { - dispatch('receiveStopSessionSuccess'); - }) - .catch((err) => { - dispatch('receiveStopSessionError', err); - }); -}; - -export const killSession = ({ commit, dispatch }) => { - dispatch('stopPollingSessionStatus'); - commit(types.SET_SESSION_STATUS, STOPPED); -}; - -export const restartSession = ({ state, dispatch, rootState }) => { - const { status, retryPath } = state.session; - const { currentBranchId } = rootState; - - if (status !== STOPPED) { - return; - } - - if (!retryPath) { - dispatch('startSession'); - return; - } - - dispatch('requestStartSession'); - - axios - .post(retryPath, { branch: currentBranchId, format: 'json' }) - .then(({ data }) => { - dispatch('receiveStartSessionSuccess', data); - }) - .catch((error) => { - const responseStatus = error.response && error.response.status; - // We may have removed the build, in this case we'll just create a new session - if ( - responseStatus === HTTP_STATUS_NOT_FOUND || - responseStatus === HTTP_STATUS_UNPROCESSABLE_ENTITY - ) { - dispatch('startSession'); - } else { - dispatch('receiveStartSessionError', error); - } - }); -}; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js deleted file mode 100644 index 463634c946d..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_status.js +++ /dev/null @@ -1,64 +0,0 @@ -import { createAlert } from '~/alert'; -import axios from '~/lib/utils/axios_utils'; -import * as messages from '../messages'; -import * as types from '../mutation_types'; -import { isEndingStatus } from '../utils'; - -export const pollSessionStatus = ({ state, dispatch, commit }) => { - dispatch('stopPollingSessionStatus'); - dispatch('fetchSessionStatus'); - - const interval = setInterval(() => { - if (!state.session) { - dispatch('stopPollingSessionStatus'); - } else { - dispatch('fetchSessionStatus'); - } - }, 5000); - - commit(types.SET_SESSION_STATUS_INTERVAL, interval); -}; - -export const stopPollingSessionStatus = ({ state, commit }) => { - const { sessionStatusInterval } = state; - - if (!sessionStatusInterval) { - return; - } - - clearInterval(sessionStatusInterval); - - commit(types.SET_SESSION_STATUS_INTERVAL, 0); -}; - -export const receiveSessionStatusSuccess = ({ commit, dispatch }, data) => { - const status = data && data.status; - - commit(types.SET_SESSION_STATUS, status); - - if (isEndingStatus(status)) { - dispatch('killSession'); - } -}; - -export const receiveSessionStatusError = ({ dispatch }) => { - createAlert({ message: messages.UNEXPECTED_ERROR_STATUS }); - dispatch('killSession'); -}; - -export const fetchSessionStatus = ({ dispatch, state }) => { - if (!state.session) { - return; - } - - const { showPath } = state.session; - - axios - .get(showPath) - .then(({ data }) => { - dispatch('receiveSessionStatusSuccess', data); - }) - .catch((error) => { - dispatch('receiveSessionStatusError', error); - }); -}; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/setup.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/setup.js deleted file mode 100644 index 78ad94f8a91..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/actions/setup.js +++ /dev/null @@ -1,14 +0,0 @@ -import * as types from '../mutation_types'; - -export const init = ({ dispatch }) => { - dispatch('fetchConfigCheck'); - dispatch('fetchRunnersCheck'); -}; - -export const hideSplash = ({ commit }) => { - commit(types.HIDE_SPLASH); -}; - -export const setPaths = ({ commit }, paths) => { - commit(types.SET_PATHS, paths); -}; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/constants.js b/app/assets/javascripts/ide/stores/modules/terminal/constants.js deleted file mode 100644 index f7ae9d8f4ea..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/constants.js +++ /dev/null @@ -1,9 +0,0 @@ -export const CHECK_CONFIG = 'config'; -export const CHECK_RUNNERS = 'runners'; -export const RETRY_RUNNERS_INTERVAL = 10000; - -export const STARTING = 'starting'; -export const PENDING = 'pending'; -export const RUNNING = 'running'; -export const STOPPING = 'stopping'; -export const STOPPED = 'stopped'; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/getters.js b/app/assets/javascripts/ide/stores/modules/terminal/getters.js deleted file mode 100644 index fb9a1a2fa39..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/getters.js +++ /dev/null @@ -1,17 +0,0 @@ -export const allCheck = (state) => { - const checks = Object.values(state.checks); - - if (checks.some((check) => check.isLoading)) { - return { isLoading: true }; - } - - const invalidCheck = checks.find((check) => !check.isValid); - const isValid = !invalidCheck; - const message = !invalidCheck ? '' : invalidCheck.message; - - return { - isLoading: false, - isValid, - message, - }; -}; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/index.js b/app/assets/javascripts/ide/stores/modules/terminal/index.js deleted file mode 100644 index ef1289e1722..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; -import state from './state'; - -export default () => ({ - namespaced: true, - actions, - getters, - mutations, - state: state(), -}); diff --git a/app/assets/javascripts/ide/stores/modules/terminal/messages.js b/app/assets/javascripts/ide/stores/modules/terminal/messages.js deleted file mode 100644 index a2b45f9dc62..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/messages.js +++ /dev/null @@ -1,58 +0,0 @@ -import { escape } from 'lodash'; -import { HTTP_STATUS_FORBIDDEN, HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status'; -import { __, sprintf } from '~/locale'; - -export const UNEXPECTED_ERROR_CONFIG = __( - 'An unexpected error occurred while checking the project environment.', -); -export const UNEXPECTED_ERROR_RUNNERS = __( - 'An unexpected error occurred while checking the project runners.', -); -export const UNEXPECTED_ERROR_STATUS = __( - 'An unexpected error occurred while communicating with the Web Terminal.', -); -export const UNEXPECTED_ERROR_STARTING = __( - 'An unexpected error occurred while starting the Web Terminal.', -); -export const UNEXPECTED_ERROR_STOPPING = __( - 'An unexpected error occurred while stopping the Web Terminal.', -); -export const EMPTY_RUNNERS = __( - 'Configure GitLab runners to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}', -); -export const ERROR_CONFIG = __( - 'Configure a %{codeStart}.gitlab-webide.yml%{codeEnd} file in the %{codeStart}.gitlab%{codeEnd} directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}', -); -export const ERROR_PERMISSION = __( - 'You do not have permission to run the Web Terminal. Please contact a project administrator.', -); - -export const configCheckError = (status, helpUrl) => { - if (status === HTTP_STATUS_UNPROCESSABLE_ENTITY) { - return sprintf( - ERROR_CONFIG, - { - helpStart: ``, - helpEnd: '', - codeStart: '', - codeEnd: '', - }, - false, - ); - } - if (status === HTTP_STATUS_FORBIDDEN) { - return ERROR_PERMISSION; - } - - return UNEXPECTED_ERROR_CONFIG; -}; - -export const runnersCheckEmpty = (helpUrl) => - sprintf( - EMPTY_RUNNERS, - { - helpStart: ``, - helpEnd: '', - }, - false, - ); diff --git a/app/assets/javascripts/ide/stores/modules/terminal/mutation_types.js b/app/assets/javascripts/ide/stores/modules/terminal/mutation_types.js deleted file mode 100644 index b6a6f28abfa..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/mutation_types.js +++ /dev/null @@ -1,11 +0,0 @@ -export const SET_VISIBLE = 'SET_VISIBLE'; -export const HIDE_SPLASH = 'HIDE_SPLASH'; -export const SET_PATHS = 'SET_PATHS'; - -export const REQUEST_CHECK = 'REQUEST_CHECK'; -export const RECEIVE_CHECK_SUCCESS = 'RECEIVE_CHECK_SUCCESS'; -export const RECEIVE_CHECK_ERROR = 'RECEIVE_CHECK_ERROR'; - -export const SET_SESSION = 'SET_SESSION'; -export const SET_SESSION_STATUS = 'SET_SESSION_STATUS'; -export const SET_SESSION_STATUS_INTERVAL = 'SET_SESSION_STATUS_INTERVAL'; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/mutations.js b/app/assets/javascripts/ide/stores/modules/terminal/mutations.js deleted file mode 100644 index 8adde8f6b4e..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/mutations.js +++ /dev/null @@ -1,64 +0,0 @@ -import * as types from './mutation_types'; - -export default { - [types.SET_VISIBLE](state, isVisible) { - Object.assign(state, { - isVisible, - }); - }, - [types.HIDE_SPLASH](state) { - Object.assign(state, { - isShowSplash: false, - }); - }, - [types.SET_PATHS](state, paths) { - Object.assign(state, { - paths, - }); - }, - [types.REQUEST_CHECK](state, type) { - Object.assign(state.checks, { - [type]: { - isLoading: true, - }, - }); - }, - [types.RECEIVE_CHECK_ERROR](state, { type, message }) { - Object.assign(state.checks, { - [type]: { - isLoading: false, - isValid: false, - message, - }, - }); - }, - [types.RECEIVE_CHECK_SUCCESS](state, type) { - Object.assign(state.checks, { - [type]: { - isLoading: false, - isValid: true, - message: null, - }, - }); - }, - [types.SET_SESSION](state, session) { - Object.assign(state, { - session, - }); - }, - [types.SET_SESSION_STATUS](state, status) { - const session = { - ...state.session, - status, - }; - - Object.assign(state, { - session, - }); - }, - [types.SET_SESSION_STATUS_INTERVAL](state, sessionStatusInterval) { - Object.assign(state, { - sessionStatusInterval, - }); - }, -}; diff --git a/app/assets/javascripts/ide/stores/modules/terminal/state.js b/app/assets/javascripts/ide/stores/modules/terminal/state.js deleted file mode 100644 index f35a10ed2fe..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/state.js +++ /dev/null @@ -1,13 +0,0 @@ -import { CHECK_CONFIG, CHECK_RUNNERS } from './constants'; - -export default () => ({ - checks: { - [CHECK_CONFIG]: { isLoading: true }, - [CHECK_RUNNERS]: { isLoading: true }, - }, - isVisible: false, - isShowSplash: true, - paths: {}, - session: null, - sessionStatusInterval: 0, -}); diff --git a/app/assets/javascripts/ide/stores/modules/terminal/utils.js b/app/assets/javascripts/ide/stores/modules/terminal/utils.js deleted file mode 100644 index 1f4bca9f50a..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal/utils.js +++ /dev/null @@ -1,5 +0,0 @@ -import { STARTING, PENDING, RUNNING } from './constants'; - -export const isStartingStatus = (status) => status === STARTING || status === PENDING; -export const isRunningStatus = (status) => status === RUNNING; -export const isEndingStatus = (status) => !isStartingStatus(status) && !isRunningStatus(status); diff --git a/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js b/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js deleted file mode 100644 index a2cb0666a99..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal_sync/actions.js +++ /dev/null @@ -1,41 +0,0 @@ -import mirror, { canConnect } from '../../../lib/mirror'; -import * as types from './mutation_types'; - -export const upload = ({ rootState, commit }) => { - commit(types.START_LOADING); - - return mirror - .upload(rootState) - .then(() => { - commit(types.SET_SUCCESS); - }) - .catch((err) => { - commit(types.SET_ERROR, err); - }); -}; - -export const stop = ({ commit }) => { - mirror.disconnect(); - - commit(types.STOP); -}; - -export const start = ({ rootState, commit }) => { - const { session } = rootState.terminal; - const path = session && session.proxyWebsocketPath; - if (!path || !canConnect(session)) { - return Promise.reject(); - } - - commit(types.START_LOADING); - - return mirror - .connect(path) - .then(() => { - commit(types.SET_SUCCESS); - }) - .catch((err) => { - commit(types.SET_ERROR, err); - throw err; - }); -}; diff --git a/app/assets/javascripts/ide/stores/modules/terminal_sync/index.js b/app/assets/javascripts/ide/stores/modules/terminal_sync/index.js deleted file mode 100644 index a0685293839..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal_sync/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import * as actions from './actions'; -import mutations from './mutations'; -import state from './state'; - -export default () => ({ - namespaced: true, - actions, - mutations, - state: state(), -}); diff --git a/app/assets/javascripts/ide/stores/modules/terminal_sync/messages.js b/app/assets/javascripts/ide/stores/modules/terminal_sync/messages.js deleted file mode 100644 index e50e1a1406b..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal_sync/messages.js +++ /dev/null @@ -1,5 +0,0 @@ -import { __ } from '~/locale'; - -export const MSG_TERMINAL_SYNC_CONNECTING = __('Connecting to terminal sync service'); -export const MSG_TERMINAL_SYNC_UPLOADING = __('Uploading changes to terminal'); -export const MSG_TERMINAL_SYNC_RUNNING = __('Terminal sync service is running'); diff --git a/app/assets/javascripts/ide/stores/modules/terminal_sync/mutation_types.js b/app/assets/javascripts/ide/stores/modules/terminal_sync/mutation_types.js deleted file mode 100644 index ec809540c18..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal_sync/mutation_types.js +++ /dev/null @@ -1,4 +0,0 @@ -export const START_LOADING = 'START_LOADING'; -export const SET_ERROR = 'SET_ERROR'; -export const SET_SUCCESS = 'SET_SUCCESS'; -export const STOP = 'STOP'; diff --git a/app/assets/javascripts/ide/stores/modules/terminal_sync/mutations.js b/app/assets/javascripts/ide/stores/modules/terminal_sync/mutations.js deleted file mode 100644 index 70ed137776a..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal_sync/mutations.js +++ /dev/null @@ -1,22 +0,0 @@ -import * as types from './mutation_types'; - -export default { - [types.START_LOADING](state) { - state.isLoading = true; - state.isError = false; - }, - [types.SET_ERROR](state, { message }) { - state.isLoading = false; - state.isError = true; - state.message = message; - }, - [types.SET_SUCCESS](state) { - state.isLoading = false; - state.isError = false; - state.isStarted = true; - }, - [types.STOP](state) { - state.isLoading = false; - state.isStarted = false; - }, -}; diff --git a/app/assets/javascripts/ide/stores/modules/terminal_sync/state.js b/app/assets/javascripts/ide/stores/modules/terminal_sync/state.js deleted file mode 100644 index 7ec3e38f675..00000000000 --- a/app/assets/javascripts/ide/stores/modules/terminal_sync/state.js +++ /dev/null @@ -1,6 +0,0 @@ -export default () => ({ - isLoading: false, - isStarted: false, - isError: false, - message: '', -}); diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js deleted file mode 100644 index ae6588f948f..00000000000 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ /dev/null @@ -1,73 +0,0 @@ -export const SET_INITIAL_DATA = 'SET_INITIAL_DATA'; -export const TOGGLE_LOADING = 'TOGGLE_LOADING'; -export const SET_LAST_COMMIT_MSG = 'SET_LAST_COMMIT_MSG'; -export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; -export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS'; -export const SET_LINKS = 'SET_LINKS'; - -// Project Mutation Types -export const SET_PROJECT = 'SET_PROJECT'; -export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT'; -export const UPDATE_PROJECT = 'UPDATE_PROJECT'; -export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; - -// Merge request mutation types -export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST'; -export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST'; -export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES'; -export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS'; - -// Branch Mutation Types -export const SET_BRANCH = 'SET_BRANCH'; -export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT'; -export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; -export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; - -// Tree mutation types -export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; -export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN'; -export const SET_TREE_OPEN = 'SET_TREE_OPEN'; -export const CREATE_TREE = 'CREATE_TREE'; -export const REMOVE_ALL_CHANGES_FILES = 'REMOVE_ALL_CHANGES_FILES'; - -// File mutation types -export const SET_FILE_DATA = 'SET_FILE_DATA'; -export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN'; -export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE'; -export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA'; -export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA'; -export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT'; -export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES'; -export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED'; -export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED'; -export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED'; -export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; -export const SET_ENTRIES = 'SET_ENTRIES'; -export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY'; -export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE'; -export const UPDATE_VIEWER = 'UPDATE_VIEWER'; -export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; - -export const CLEAR_STAGED_CHANGES = 'CLEAR_STAGED_CHANGES'; -export const STAGE_CHANGE = 'STAGE_CHANGE'; -export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE'; -export const REMOVE_FILE_FROM_STAGED_AND_CHANGED = 'REMOVE_FILE_FROM_STAGED_AND_CHANGED'; - -export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; -export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; -export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; - -export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW'; -export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; -export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; - -export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; -export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; - -export const SET_ERROR_MESSAGE = 'SET_ERROR_MESSAGE'; - -export const DELETE_ENTRY = 'DELETE_ENTRY'; -export const RENAME_ENTRY = 'RENAME_ENTRY'; -export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY'; - -export const RESTORE_TREE = 'RESTORE_TREE'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js deleted file mode 100644 index 10eaa270967..00000000000 --- a/app/assets/javascripts/ide/stores/mutations.js +++ /dev/null @@ -1,250 +0,0 @@ -import * as types from './mutation_types'; -import branchMutations from './mutations/branch'; -import fileMutations from './mutations/file'; -import mergeRequestMutation from './mutations/merge_request'; -import projectMutations from './mutations/project'; -import treeMutations from './mutations/tree'; -import { - sortTree, - swapInParentTreeWithSorting, - updateFileCollections, - removeFromParentTree, - pathsAreEqual, -} from './utils'; - -export default { - [types.SET_INITIAL_DATA](state, data) { - Object.assign(state, data); - }, - [types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) { - if (entry.path) { - Object.assign(state.entries[entry.path], { - loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading, - }); - } else { - Object.assign(entry, { - loading: forceValue !== undefined ? forceValue : !entry.loading, - }); - } - }, - [types.SET_RESIZING_STATUS](state, resizing) { - Object.assign(state, { - panelResizing: resizing, - }); - }, - [types.SET_LAST_COMMIT_MSG](state, lastCommitMsg) { - Object.assign(state, { - lastCommitMsg, - }); - }, - [types.CLEAR_STAGED_CHANGES](state) { - Object.assign(state, { - stagedFiles: [], - }); - }, - [types.SET_ENTRIES](state, entries) { - Object.assign(state, { - entries, - }); - }, - [types.CREATE_TMP_ENTRY](state, { data }) { - Object.keys(data.entries).reduce((acc, key) => { - const entry = data.entries[key]; - const foundEntry = state.entries[key]; - - // NOTE: We can't clone `entry` in any of the below assignments because - // we need `state.entries` and the `entry.tree` to reference the same object. - if (!foundEntry || foundEntry.deleted) { - Object.assign(state.entries, { - [key]: entry, - }); - } else { - const tree = entry.tree.filter( - (f) => foundEntry.tree.find((e) => e.path === f.path) === undefined, - ); - Object.assign(foundEntry, { - tree: sortTree(foundEntry.tree.concat(tree)), - }); - } - - return acc.concat(key); - }, []); - - const currentTree = state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; - const foundEntry = currentTree.tree.find((e) => e.path === data.treeList[0].path); - - if (!foundEntry) { - Object.assign(currentTree, { - tree: sortTree(currentTree.tree.concat(data.treeList)), - }); - } - }, - [types.UPDATE_TEMP_FLAG](state, { path, tempFile }) { - Object.assign(state.entries[path], { - tempFile, - changed: tempFile, - }); - }, - [types.UPDATE_VIEWER](state, viewer) { - Object.assign(state, { - viewer, - }); - }, - [types.UPDATE_DELAY_VIEWER_CHANGE](state, delayViewerUpdated) { - Object.assign(state, { - delayViewerUpdated, - }); - }, - [types.UPDATE_ACTIVITY_BAR_VIEW](state, currentActivityView) { - Object.assign(state, { - currentActivityView, - }); - }, - [types.SET_EMPTY_STATE_SVGS]( - state, - { - emptyStateSvgPath, - noChangesStateSvgPath, - committedStateSvgPath, - pipelinesEmptyStateSvgPath, - switchEditorSvgPath, - }, - ) { - Object.assign(state, { - emptyStateSvgPath, - noChangesStateSvgPath, - committedStateSvgPath, - pipelinesEmptyStateSvgPath, - switchEditorSvgPath, - }); - }, - [types.TOGGLE_FILE_FINDER](state, fileFindVisible) { - Object.assign(state, { - fileFindVisible, - }); - }, - [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) { - const changedFile = state.changedFiles.find((f) => f.path === file.path); - const { prevPath } = file; - - Object.assign(state.entries[file.path], { - raw: file.content, - changed: Boolean(changedFile), - staged: false, - lastCommitSha: lastCommit.commit.id, - - prevId: undefined, - prevPath: undefined, - prevName: undefined, - prevKey: undefined, - prevParentPath: undefined, - }); - - if (prevPath) { - // Update URLs after file has moved - const regex = new RegExp(`${prevPath}$`); - - Object.assign(state.entries[file.path], { - rawPath: file.rawPath.replace(regex, file.path), - }); - } - }, - [types.SET_LINKS](state, links) { - Object.assign(state, { links }); - }, - [types.CLEAR_PROJECTS](state) { - Object.assign(state, { projects: {}, trees: {} }); - }, - [types.RESET_OPEN_FILES](state) { - Object.assign(state, { openFiles: [] }); - }, - [types.SET_ERROR_MESSAGE](state, errorMessage) { - Object.assign(state, { errorMessage }); - }, - [types.DELETE_ENTRY](state, path) { - const entry = state.entries[path]; - const { tempFile = false } = entry; - const parent = entry.parentPath - ? state.entries[entry.parentPath] - : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; - - entry.deleted = true; - - if (parent) { - parent.tree = parent.tree.filter((f) => f.path !== entry.path); - } - - if (entry.type === 'blob') { - if (tempFile) { - // Since we only support one list of file changes, it's safe to just remove from both - // changed and staged. Otherwise, we'd need to somehow evaluate the difference between - // changed and HEAD. - // https://gitlab.com/gitlab-org/create-stage/-/issues/12669 - state.changedFiles = state.changedFiles.filter((f) => f.path !== path); - state.stagedFiles = state.stagedFiles.filter((f) => f.path !== path); - } else { - state.changedFiles = state.changedFiles.concat(entry); - } - } - }, - [types.RENAME_ENTRY](state, { path, name, parentPath }) { - const oldEntry = state.entries[path]; - const newPath = parentPath ? `${parentPath}/${name}` : name; - const isRevert = newPath === oldEntry.prevPath; - const newKey = oldEntry.key.replace(new RegExp(oldEntry.path, 'g'), newPath); - - const baseProps = { - ...oldEntry, - name, - id: newPath, - path: newPath, - key: newKey, - parentPath: parentPath || '', - }; - - const prevProps = - oldEntry.tempFile || isRevert - ? { - prevId: undefined, - prevPath: undefined, - prevName: undefined, - prevKey: undefined, - prevParentPath: undefined, - } - : { - prevId: oldEntry.prevId || oldEntry.id, - prevPath: oldEntry.prevPath || oldEntry.path, - prevName: oldEntry.prevName || oldEntry.name, - prevKey: oldEntry.prevKey || oldEntry.key, - prevParentPath: oldEntry.prevParentPath || oldEntry.parentPath, - }; - - state.entries = { - ...state.entries, - [newPath]: { - ...baseProps, - ...prevProps, - }, - }; - - if (pathsAreEqual(oldEntry.parentPath, parentPath)) { - swapInParentTreeWithSorting(state, oldEntry.key, newPath, parentPath); - } else { - removeFromParentTree(state, oldEntry.key, oldEntry.parentPath); - swapInParentTreeWithSorting(state, oldEntry.key, newPath, parentPath); - } - - if (oldEntry.type === 'blob') { - updateFileCollections(state, oldEntry.key, newPath); - } - const stateCopy = { ...state.entries }; - delete stateCopy[oldEntry.path]; - state.entries = stateCopy; - }, - - ...projectMutations, - ...mergeRequestMutation, - ...fileMutations, - ...treeMutations, - ...branchMutations, -}; diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js deleted file mode 100644 index 6afd8de2aa4..00000000000 --- a/app/assets/javascripts/ide/stores/mutations/branch.js +++ /dev/null @@ -1,37 +0,0 @@ -import * as types from '../mutation_types'; - -export default { - [types.SET_CURRENT_BRANCH](state, currentBranchId) { - Object.assign(state, { - currentBranchId, - }); - }, - [types.SET_BRANCH](state, { projectPath, branchName, branch }) { - Object.assign(state.projects[projectPath], { - branches: { - [branchName]: { - ...branch, - treeId: `${projectPath}/${branchName}`, - active: true, - workingReference: '', - }, - }, - }); - }, - [types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) { - if (!state.projects[projectId].branches[branchId]) { - Object.assign(state.projects[projectId].branches, { - [branchId]: {}, - }); - } - - Object.assign(state.projects[projectId].branches[branchId], { - workingReference: reference, - }); - }, - [types.SET_BRANCH_COMMIT](state, { projectId, branchId, commit }) { - Object.assign(state.projects[projectId].branches[branchId], { - commit, - }); - }, -}; diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js deleted file mode 100644 index fa9830a7469..00000000000 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ /dev/null @@ -1,241 +0,0 @@ -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { diffModes } from '../../constants'; -import * as types from '../mutation_types'; -import { sortTree } from '../utils'; - -export default { - [types.SET_FILE_ACTIVE](state, { path, active }) { - Object.assign(state.entries[path], { - active, - lastOpenedAt: new Date().getTime(), - }); - - if (active && !state.entries[path].pending) { - Object.assign(state, { - openFiles: state.openFiles.map((f) => - Object.assign(f, { active: f.pending ? false : f.active }), - ), - }); - } - }, - [types.TOGGLE_FILE_OPEN](state, path) { - const entry = state.entries[path]; - - entry.opened = !entry.opened; - if (entry.opened && !entry.tempFile) { - entry.loading = true; - } - - if (entry.opened) { - Object.assign(state, { - openFiles: state.openFiles.filter((f) => f.path !== path).concat(state.entries[path]), - }); - } else { - Object.assign(state, { - openFiles: state.openFiles.filter((f) => f.key !== entry.key), - }); - } - }, - [types.SET_FILE_DATA](state, { data, file }) { - const stateEntry = state.entries[file.path]; - const stagedFile = state.stagedFiles.find((f) => f.path === file.path); - const openFile = state.openFiles.find((f) => f.path === file.path); - const changedFile = state.changedFiles.find((f) => f.path === file.path); - - [stateEntry, stagedFile, openFile, changedFile].forEach((f) => { - if (f) { - Object.assign( - f, - convertObjectPropsToCamelCase(data, { dropKeys: ['path', 'name', 'raw', 'baseRaw'] }), - { - raw: (stateEntry && stateEntry.raw) || null, - baseRaw: null, - }, - ); - } - }); - }, - [types.SET_FILE_RAW_DATA](state, { file, raw, fileDeletedAndReadded = false }) { - const openPendingFile = state.openFiles.find( - (f) => - f.path === file.path && f.pending && !(f.tempFile && !f.prevPath && !fileDeletedAndReadded), - ); - const stagedFile = state.stagedFiles.find((f) => f.path === file.path); - - if (file.tempFile && file.content === '' && !fileDeletedAndReadded) { - Object.assign(state.entries[file.path], { content: raw }); - } else if (fileDeletedAndReadded) { - Object.assign(stagedFile, { raw }); - } else { - Object.assign(state.entries[file.path], { raw }); - } - - if (!openPendingFile) return; - - if (!openPendingFile.tempFile) { - openPendingFile.raw = raw; - } else if (openPendingFile.tempFile && !fileDeletedAndReadded) { - openPendingFile.content = raw; - } else if (fileDeletedAndReadded) { - Object.assign(stagedFile, { raw }); - } - }, - [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) { - Object.assign(state.entries[file.path], { - baseRaw, - }); - }, - [types.UPDATE_FILE_CONTENT](state, { path, content }) { - const stagedFile = state.stagedFiles.find((f) => f.path === path); - const rawContent = stagedFile ? stagedFile.content : state.entries[path].raw; - const changed = content !== rawContent; - - Object.assign(state.entries[path], { - content, - changed, - }); - }, - [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) { - let diffMode = diffModes.replaced; - if (mrChange.new_file) { - diffMode = diffModes.new; - } else if (mrChange.deleted_file) { - diffMode = diffModes.deleted; - } else if (mrChange.renamed_file) { - diffMode = diffModes.renamed; - } - Object.assign(state.entries[file.path], { - mrChange: { - ...mrChange, - diffMode, - }, - }); - }, - [types.DISCARD_FILE_CHANGES](state, path) { - const stagedFile = state.stagedFiles.find((f) => f.path === path); - const entry = state.entries[path]; - const { deleted } = entry; - - Object.assign(state.entries[path], { - content: stagedFile ? stagedFile.content : state.entries[path].raw, - changed: false, - deleted: false, - }); - - if (deleted) { - const parent = entry.parentPath - ? state.entries[entry.parentPath] - : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; - - parent.tree = sortTree(parent.tree.concat(entry)); - } - }, - [types.ADD_FILE_TO_CHANGED](state, path) { - Object.assign(state, { - changedFiles: state.changedFiles.concat(state.entries[path]), - }); - }, - [types.REMOVE_FILE_FROM_CHANGED](state, path) { - Object.assign(state, { - changedFiles: state.changedFiles.filter((f) => f.path !== path), - }); - }, - [types.STAGE_CHANGE](state, { path, diffInfo }) { - const stagedFile = state.stagedFiles.find((f) => f.path === path); - - Object.assign(state, { - changedFiles: state.changedFiles.filter((f) => f.path !== path), - entries: Object.assign(state.entries, { - [path]: Object.assign(state.entries[path], { - staged: diffInfo.exists, - changed: diffInfo.changed, - tempFile: diffInfo.tempFile, - deleted: diffInfo.deleted, - }), - }), - }); - - if (stagedFile) { - Object.assign(stagedFile, { ...state.entries[path] }); - } else { - state.stagedFiles = [...state.stagedFiles, { ...state.entries[path] }]; - } - - if (!diffInfo.exists) { - state.stagedFiles = state.stagedFiles.filter((f) => f.path !== path); - } - }, - [types.UNSTAGE_CHANGE](state, { path, diffInfo }) { - const changedFile = state.changedFiles.find((f) => f.path === path); - const stagedFile = state.stagedFiles.find((f) => f.path === path); - - if (!changedFile && stagedFile) { - Object.assign(state.entries[path], { - ...stagedFile, - key: state.entries[path].key, - active: state.entries[path].active, - opened: state.entries[path].opened, - changed: true, - }); - - state.changedFiles = state.changedFiles.concat(state.entries[path]); - } - - if (!diffInfo.exists) { - state.changedFiles = state.changedFiles.filter((f) => f.path !== path); - } - - Object.assign(state, { - stagedFiles: state.stagedFiles.filter((f) => f.path !== path), - entries: Object.assign(state.entries, { - [path]: Object.assign(state.entries[path], { - staged: false, - changed: diffInfo.changed, - tempFile: diffInfo.tempFile, - deleted: diffInfo.deleted, - }), - }), - }); - }, - [types.TOGGLE_FILE_CHANGED](state, { file, changed }) { - Object.assign(state.entries[file.path], { - changed, - }); - }, - [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) { - state.entries[file.path].opened = false; - state.entries[file.path].active = false; - state.entries[file.path].lastOpenedAt = new Date().getTime(); - state.openFiles.forEach((f) => - Object.assign(f, { - opened: false, - active: false, - }), - ); - state.openFiles = [ - { - ...file, - key: `${keyPrefix}-${file.key}`, - pending: true, - opened: true, - active: true, - }, - ]; - }, - [types.REMOVE_PENDING_TAB](state, file) { - Object.assign(state, { - openFiles: state.openFiles.filter((f) => f.key !== file.key), - }); - }, - [types.REMOVE_FILE_FROM_STAGED_AND_CHANGED](state, file) { - Object.assign(state, { - changedFiles: state.changedFiles.filter((f) => f.key !== file.key), - stagedFiles: state.stagedFiles.filter((f) => f.key !== file.key), - }); - - Object.assign(state.entries[file.path], { - changed: false, - staged: false, - }); - }, -}; diff --git a/app/assets/javascripts/ide/stores/mutations/merge_request.js b/app/assets/javascripts/ide/stores/mutations/merge_request.js deleted file mode 100644 index e5b5107bc93..00000000000 --- a/app/assets/javascripts/ide/stores/mutations/merge_request.js +++ /dev/null @@ -1,36 +0,0 @@ -import * as types from '../mutation_types'; - -export default { - [types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) { - Object.assign(state, { - currentMergeRequestId, - }); - }, - [types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) { - const existingMergeRequest = state.projects[projectPath].mergeRequests[mergeRequestId] || {}; - - Object.assign(state.projects[projectPath], { - mergeRequests: { - [mergeRequestId]: { - ...mergeRequest, - active: true, - changes: [], - versions: [], - baseCommitSha: null, - ...existingMergeRequest, - }, - }, - }); - }, - [types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) { - Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], { - changes, - }); - }, - [types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) { - Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], { - versions, - baseCommitSha: versions.length ? versions[0].base_commit_sha : null, - }); - }, -}; diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js deleted file mode 100644 index 57ea2a75c6d..00000000000 --- a/app/assets/javascripts/ide/stores/mutations/project.js +++ /dev/null @@ -1,44 +0,0 @@ -import * as types from '../mutation_types'; - -export default { - [types.SET_CURRENT_PROJECT](state, currentProjectId) { - Object.assign(state, { - currentProjectId, - }); - }, - [types.SET_PROJECT](state, { projectPath, project }) { - // Add client side properties - Object.assign(project, { - tree: [], - branches: {}, - mergeRequests: {}, - active: true, - }); - - Object.assign(state, { - projects: { ...state.projects, [projectPath]: project }, - }); - }, - [types.TOGGLE_EMPTY_STATE](state, { projectPath, value }) { - Object.assign(state.projects[projectPath], { - empty_repo: value, - }); - }, - [types.UPDATE_PROJECT](state, { projectPath, props }) { - const project = state.projects[projectPath]; - - if (!project || !props) { - return; - } - - Object.keys(props).reduce((acc, key) => { - project[key] = props[key]; - return project; - }, project); - - state.projects = { - ...state.projects, - [projectPath]: project, - }; - }, -}; diff --git a/app/assets/javascripts/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js deleted file mode 100644 index c38002bd4e0..00000000000 --- a/app/assets/javascripts/ide/stores/mutations/tree.js +++ /dev/null @@ -1,52 +0,0 @@ -import * as types from '../mutation_types'; -import { sortTree, mergeTrees } from '../utils'; - -export default { - [types.TOGGLE_TREE_OPEN](state, path) { - Object.assign(state.entries[path], { - opened: !state.entries[path].opened, - }); - }, - [types.SET_TREE_OPEN](state, path) { - Object.assign(state.entries[path], { - opened: true, - }); - }, - [types.CREATE_TREE](state, { treePath }) { - Object.assign(state, { - trees: { - ...state.trees, - [treePath]: { - tree: [], - loading: true, - }, - }, - }); - }, - [types.SET_DIRECTORY_DATA](state, { data, treePath }) { - const selectedTree = state.trees[treePath]; - - // If we opened files while loading the tree, we need to merge them - // Otherwise, simply overwrite the tree - const tree = !selectedTree.tree.length - ? data - : selectedTree.loading && mergeTrees(selectedTree.tree, data); - - Object.assign(selectedTree, { tree }); - }, - [types.REMOVE_ALL_CHANGES_FILES](state) { - Object.assign(state, { - changedFiles: [], - }); - }, - [types.RESTORE_TREE](state, path) { - const entry = state.entries[path]; - const parent = entry.parentPath - ? state.entries[entry.parentPath] - : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; - - if (!parent.tree.find((f) => f.path === path)) { - parent.tree = sortTree(parent.tree.concat(entry)); - } - }, -}; diff --git a/app/assets/javascripts/ide/stores/plugins/terminal.js b/app/assets/javascripts/ide/stores/plugins/terminal.js deleted file mode 100644 index f7ed3075b0c..00000000000 --- a/app/assets/javascripts/ide/stores/plugins/terminal.js +++ /dev/null @@ -1,25 +0,0 @@ -import * as mutationTypes from '~/ide/stores/mutation_types'; -import terminalModule from '../modules/terminal'; - -function getPathsFromData(el) { - return { - webTerminalSvgPath: el.dataset.webTerminalSvgPath, - webTerminalHelpPath: el.dataset.webTerminalHelpPath, - webTerminalConfigHelpPath: el.dataset.webTerminalConfigHelpPath, - webTerminalRunnersHelpPath: el.dataset.webTerminalRunnersHelpPath, - }; -} - -export default function createTerminalPlugin(el) { - return (store) => { - store.registerModule('terminal', terminalModule()); - - store.dispatch('terminal/setPaths', getPathsFromData(el)); - - store.subscribe(({ type }) => { - if (type === mutationTypes.SET_BRANCH_WORKING_REFERENCE) { - store.dispatch('terminal/init'); - } - }); - }; -} diff --git a/app/assets/javascripts/ide/stores/plugins/terminal_sync.js b/app/assets/javascripts/ide/stores/plugins/terminal_sync.js deleted file mode 100644 index 944a034fe97..00000000000 --- a/app/assets/javascripts/ide/stores/plugins/terminal_sync.js +++ /dev/null @@ -1,59 +0,0 @@ -import { debounce } from 'lodash'; -import { commitActionTypes } from '~/ide/constants'; -import eventHub from '~/ide/eventhub'; -import { isEndingStatus, isRunningStatus } from '../modules/terminal/utils'; -import terminalSyncModule from '../modules/terminal_sync'; - -const UPLOAD_DEBOUNCE = 200; - -/** - * Registers and controls the terminalSync vuex module based on IDE events. - * - * - Watches the terminal session status state to control start/stop. - * - Listens for file change event to control upload. - */ -export default function createMirrorPlugin() { - return (store) => { - store.registerModule('terminalSync', terminalSyncModule()); - - const upload = debounce(() => { - store.dispatch(`terminalSync/upload`); - }, UPLOAD_DEBOUNCE); - - const onFilesChange = (payload) => { - // Do nothing on a file update since we only want to trigger manually on "save". - if (payload?.type === commitActionTypes.update) { - return; - } - - upload(); - }; - - const stop = () => { - store.dispatch(`terminalSync/stop`); - eventHub.$off('ide.files.change', onFilesChange); - }; - - const start = () => { - store - .dispatch(`terminalSync/start`) - .then(() => { - eventHub.$on('ide.files.change', onFilesChange); - }) - .catch(() => { - // error is handled in store - }); - }; - - store.watch( - (x) => x.terminal && x.terminal.session && x.terminal.session.status, - (val) => { - if (isRunningStatus(val)) { - start(); - } else if (isEndingStatus(val)) { - stop(); - } - }, - ); - }; -} diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js deleted file mode 100644 index 6297231e252..00000000000 --- a/app/assets/javascripts/ide/stores/state.js +++ /dev/null @@ -1,33 +0,0 @@ -import { leftSidebarViews, viewerTypes } from '../constants'; -import { DEFAULT_THEME } from '../lib/themes'; - -export default () => ({ - currentProjectId: '', - currentBranchId: '', - currentMergeRequestId: '', - changedFiles: [], - stagedFiles: [], - endpoints: {}, - lastCommitMsg: '', - loading: false, - openFiles: [], - trees: {}, - projects: {}, - panelResizing: false, - entries: {}, - viewer: viewerTypes.edit, - delayViewerUpdated: false, - currentActivityView: leftSidebarViews.edit.name, - fileFindVisible: false, - links: {}, - errorMessage: null, - entryModal: { - type: '', - path: '', - entry: {}, - }, - renderWhitespaceInCode: false, - editorTheme: DEFAULT_THEME, - previewMarkdownPath: '', - userPreferencesPath: '', -}); diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index ae51dae60db..f84104a47ed 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -1,11 +1,3 @@ -import { - relativePathToAbsolute, - isAbsolute, - isRootRelative, - isBlobUrl, -} from '~/lib/utils/url_utility'; -import { commitActionTypes } from '../constants'; - export const dataStructure = () => ({ id: '', // Key will contain a mixture of ID and path @@ -69,67 +61,6 @@ export const decorateData = (entity) => { }); }; -export const setPageTitle = (title) => { - document.title = title; -}; - -export const setPageTitleForFile = (state, file) => { - const title = [file.path, state.currentBranchId, state.currentProjectId, 'GitLab'].join(' · '); - setPageTitle(title); -}; - -export const commitActionForFile = (file) => { - if (file.prevPath) { - return commitActionTypes.move; - } - if (file.deleted) { - return commitActionTypes.delete; - } - if (file.tempFile) { - return commitActionTypes.create; - } - - return commitActionTypes.update; -}; - -export const getCommitFiles = (stagedFiles) => - stagedFiles.reduce((acc, file) => { - if (file.type === 'tree') return acc; - - return acc.concat({ - ...file, - }); - }, []); - -export const createCommitPayload = ({ - branch, - getters, - newBranch, - state, - rootState, - rootGetters, -}) => ({ - branch, - commit_message: state.commitMessage || getters.preBuiltCommitMessage, - actions: getCommitFiles(rootState.stagedFiles).map((f) => { - const isBlob = isBlobUrl(f.rawPath); - const content = isBlob ? btoa(f.content) : f.content; - - return { - action: commitActionForFile(f), - file_path: f.path, - previous_path: f.prevPath || undefined, - content: content || undefined, - encoding: isBlob ? 'base64' : 'text', - last_commit_id: newBranch || f.deleted || f.prevPath ? undefined : f.lastCommitSha, - }; - }), - start_sha: newBranch ? rootGetters.lastCommit.id : undefined, -}); - -export const createNewMergeRequestUrl = (projectUrl, source, target) => - `${projectUrl}/-/merge_requests/new?merge_request[source_branch]=${source}&merge_request[target_branch]=${target}&nav_source=webide`; - const sortTreesByTypeAndName = (a, b) => { if (a.type === 'tree' && b.type === 'blob') { return -1; @@ -158,125 +89,3 @@ export const sortTree = (sortedTree) => }), ) .sort(sortTreesByTypeAndName); - -export const filePathMatches = (filePath, path) => filePath.indexOf(`${path}/`) === 0; - -export const getChangesCountForFiles = (files, path) => - files.filter((f) => filePathMatches(f.path, path)).length; - -export const mergeTrees = (fromTree, toTree) => { - if (!fromTree || !fromTree.length) { - return toTree; - } - - const recurseTree = (n, t) => { - if (!n) { - return t; - } - const existingTreeNode = t.find((el) => el.path === n.path); - - if (existingTreeNode && n.tree.length > 0) { - existingTreeNode.opened = true; - recurseTree(n.tree[0], existingTreeNode.tree); - } else if (!existingTreeNode) { - const sorted = sortTree(t.concat(n)); - t.splice(0, t.length + 1, ...sorted); - } - return t; - }; - - for (let i = 0, l = fromTree.length; i < l; i += 1) { - recurseTree(fromTree[i], toTree); - } - - return toTree; -}; - -// eslint-disable-next-line max-params -export const swapInStateArray = (state, arr, key, entryPath) => - Object.assign(state, { - [arr]: state[arr].map((f) => (f.key === key ? state.entries[entryPath] : f)), - }); - -export const getEntryOrRoot = (state, path) => - path ? state.entries[path] : state.trees[`${state.currentProjectId}/${state.currentBranchId}`]; - -// eslint-disable-next-line max-params -export const swapInParentTreeWithSorting = (state, oldKey, newPath, parentPath) => { - if (!newPath) { - return; - } - - const parent = getEntryOrRoot(state, parentPath); - - if (parent) { - const tree = parent.tree - // filter out old entry && new entry - .filter(({ key, path }) => key !== oldKey && path !== newPath) - // concat new entry - .concat(state.entries[newPath]); - - parent.tree = sortTree(tree); - } -}; - -export const removeFromParentTree = (state, oldKey, parentPath) => { - const parent = getEntryOrRoot(state, parentPath); - - if (parent) { - parent.tree = sortTree(parent.tree.filter(({ key }) => key !== oldKey)); - } -}; - -export const updateFileCollections = (state, key, entryPath) => { - ['openFiles', 'changedFiles', 'stagedFiles'].forEach((fileCollection) => { - swapInStateArray(state, fileCollection, key, entryPath); - }); -}; - -export const cleanTrailingSlash = (path) => path.replace(/\/$/, ''); - -export const pathsAreEqual = (a, b) => { - const cleanA = a ? cleanTrailingSlash(a) : ''; - const cleanB = b ? cleanTrailingSlash(b) : ''; - - return cleanA === cleanB; -}; - -export function extractMarkdownImagesFromEntries(mdFile, entries) { - /** - * Regex to identify an image tag in markdown, like: - * - * ![img alt goes here](/img.png) - * ![img alt](../img 1/img.png "my image title") - * ![img alt](https://gitlab.com/assets/logo.svg "title here") - * - */ - const reMdImage = /!\[([^\]]*)\]\((.*?)(?:(?="|\))"([^"]*)")?\)/gi; - const prefix = 'gl_md_img_'; - const images = {}; - - let content = mdFile.content || mdFile.raw; - let i = 0; - - // eslint-disable-next-line max-params - content = content.replace(reMdImage, (_, alt, path, title) => { - const imagePath = (isRootRelative(path) ? path : relativePathToAbsolute(path, mdFile.path)) - .substr(1) - .trim(); - - const imageContent = entries[imagePath]?.content || entries[imagePath]?.raw; - const imageRawPath = entries[imagePath]?.rawPath; - - if (!isAbsolute(path) && imageContent) { - const src = imageRawPath; - i += 1; - const key = `{{${prefix}${i}}}`; - images[key] = { alt, src, title }; - return key; - } - return title ? `![${alt}](${path}"${title}")` : `![${alt}](${path})`; - }); - - return { content, images }; -} diff --git a/app/assets/javascripts/ide/sync_router_and_store.js b/app/assets/javascripts/ide/sync_router_and_store.js deleted file mode 100644 index d73ac93dc1d..00000000000 --- a/app/assets/javascripts/ide/sync_router_and_store.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * This method adds listeners to the given router and store and syncs their state with eachother - * - * ### Why? - * - * Previously the IDE had a circular dependency between a singleton router and a singleton store. - * This causes some integration testing headaches... - * - * At the time, the most effecient way to break this ciruclar dependency was to: - * - * - Replace the router with a factory function that receives a store reference - * - Have the store write to a certain state that can be watched by the router - * - * Hence... This helper function... - */ -export const syncRouterAndStore = (router, store) => { - const disposables = []; - - let currentPath = ''; - - // sync store to router - disposables.push( - store.watch( - (state) => state.router.fullPath, - (fullPath) => { - if (currentPath === fullPath) { - return; - } - - currentPath = fullPath; - - router.push(fullPath); - }, - ), - ); - - // sync router to store - disposables.push( - router.afterEach((to) => { - if (currentPath === to.fullPath) { - return; - } - - currentPath = to.fullPath; - store.dispatch('router/push', currentPath, { root: true }); - }), - ); - - const unsync = () => { - disposables.forEach((fn) => fn()); - }; - - return unsync; -}; diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js index 47cac5f103a..38b622cc886 100644 --- a/app/assets/javascripts/ide/utils.js +++ b/app/assets/javascripts/ide/utils.js @@ -1,76 +1,5 @@ -import { flatten, isString } from 'lodash'; import { languages } from 'monaco-editor'; import { setDiagnosticsOptions as yamlDiagnosticsOptions } from 'monaco-yaml'; -import { performanceMarkAndMeasure } from '~/performance/utils'; -import { SIDE_LEFT, SIDE_RIGHT } from './constants'; - -const toLowerCase = (x) => x.toLowerCase(); - -const monacoLanguages = languages.getLanguages(); -const monacoExtensions = new Set( - flatten(monacoLanguages.map((lang) => lang.extensions?.map(toLowerCase) || [])), -); -const monacoMimetypes = new Set( - flatten(monacoLanguages.map((lang) => lang.mimetypes?.map(toLowerCase) || [])), -); -const monacoFilenames = new Set( - flatten(monacoLanguages.map((lang) => lang.filenames?.map(toLowerCase) || [])), -); - -const KNOWN_TYPES = [ - { - isText: false, - isMatch(mimeType) { - return mimeType.toLowerCase().includes('image/'); - }, - }, - { - isText: true, - isMatch(mimeType) { - return mimeType.toLowerCase().includes('text/'); - }, - }, - { - isText: true, - isMatch(mimeType, fileName) { - const fileExtension = fileName.includes('.') ? `.${fileName.split('.').pop()}` : ''; - - return ( - monacoExtensions.has(fileExtension.toLowerCase()) || - monacoMimetypes.has(mimeType.toLowerCase()) || - monacoFilenames.has(fileName.toLowerCase()) - ); - }, - }, -]; - -export function isTextFile({ name, raw, binary, content, mimeType = '' }) { - // some file objects already have a `binary` property set on them. If so, use it first - if (typeof binary === 'boolean') return !binary; - - const knownType = KNOWN_TYPES.find((type) => type.isMatch(mimeType, name)); - if (knownType) return knownType.isText; - - // does the string contain ascii characters only (ranges from space to tilde, tabs and new lines) - const asciiRegex = /^[ -~\t\n\r]+$/; - - const fileContents = raw || content; - - // for unknown types, determine the type by evaluating the file contents - return isString(fileContents) && (fileContents === '' || asciiRegex.test(fileContents)); -} - -export const createPathWithExt = (p) => { - const ext = p.lastIndexOf('.') >= 0 ? p.substring(p.lastIndexOf('.') + 1) : ''; - - return `${p.substring(1, p.lastIndexOf('.') + 1 || p.length)}${ext || '.js'}`; -}; - -export const trimPathComponents = (path) => - path - .split('/') - .map((s) => s.trim()) - .join('/'); export function registerLanguages(def, ...defs) { defs.forEach((lang) => registerLanguages(lang)); @@ -94,78 +23,3 @@ export function registerSchema(schema, options = {}) { languages.json.jsonDefaults.setDiagnosticsOptions(defaultOptions); yamlDiagnosticsOptions(defaultOptions); } - -export const otherSide = (side) => (side === SIDE_RIGHT ? SIDE_LEFT : SIDE_RIGHT); - -export function trimTrailingWhitespace(content) { - return content.replace(/[^\S\r\n]+$/gm, ''); -} - -export function getPathParents(path, maxDepth = Infinity) { - const pathComponents = path.split('/'); - const paths = []; - - let depth = 0; - while (pathComponents.length && depth < maxDepth) { - pathComponents.pop(); - - let parentPath = pathComponents.join('/'); - if (parentPath.startsWith('/')) parentPath = parentPath.slice(1); - if (parentPath) paths.push(parentPath); - - depth += 1; - } - - return paths; -} - -export function getPathParent(path) { - return getPathParents(path, 1)[0]; -} - -export function getFileEOL(content = '') { - return content.includes('\r\n') ? 'CRLF' : 'LF'; -} - -/** - * Adds or increments the numeric suffix to a filename/branch name. - * Retains underscore or dash before the numeric suffix if it already exists. - * - * Examples: - * hello -> hello-1 - * hello-2425 -> hello-2425 - * hello.md -> hello-1.md - * hello_2.md -> hello_3.md - * hello_ -> hello_1 - * main-patch-22432 -> main-patch-22433 - * patch_332 -> patch_333 - * - * @param {string} filename File name or branch name - * @param {number} [randomize] Should randomize the numeric suffix instead of auto-incrementing? - */ -export function addNumericSuffix(filename, randomize = false) { - // eslint-disable-next-line max-params - return filename.replace(/([ _-]?)(\d*)(\..+?$|$)/, (_, before, number, after) => { - const n = randomize ? Math.random().toString().substring(2, 7).slice(-5) : Number(number) + 1; - return `${before || '-'}${n}${after}`; - }); -} - -export const measurePerformance = ( - mark, - measureName, - measureStart = undefined, - measureEnd = mark, - // eslint-disable-next-line max-params -) => { - performanceMarkAndMeasure({ - mark, - measures: [ - { - name: measureName, - start: measureStart, - end: measureEnd, - }, - ], - }); -}; diff --git a/app/assets/javascripts/pages/ide/index/index.js b/app/assets/javascripts/pages/ide/index/index.js index 15933256e75..d192df3561e 100644 --- a/app/assets/javascripts/pages/ide/index/index.js +++ b/app/assets/javascripts/pages/ide/index/index.js @@ -1,4 +1,3 @@ import { startIde } from '~/ide/index'; -import extendStore from '~/ide/stores/extend'; -startIde({ extendStore }); +startIde(); diff --git a/app/assets/stylesheets/page_bundles/_ide_mixins.scss b/app/assets/stylesheets/page_bundles/_ide_mixins.scss deleted file mode 100644 index 02d8c94e6ce..00000000000 --- a/app/assets/stylesheets/page_bundles/_ide_mixins.scss +++ /dev/null @@ -1,17 +0,0 @@ -@mixin ide-trace-view { - display: flex; - flex-direction: column; - height: 100%; - - .top-bar { - @include build-log-bar(35px); - - top: 0; - font-size: 12px; - border-top-right-radius: $gl-border-radius-base; - - .controllers { - @include build-controllers(15px, center, false, 0, inline, 0); - } - } -} diff --git a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss deleted file mode 100644 index 5e56a540686..00000000000 --- a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss +++ /dev/null @@ -1,129 +0,0 @@ -.blob-editor-container { - flex: 1; - height: 0; - display: flex; - flex-direction: column; - justify-content: center; - - .monaco-editor .lines-content .cigr { - display: none; - } - - .is-readonly .editor.original { - .view-lines { - cursor: default; - } - - .cursors-layer { - display: none; - } - } - - .is-deleted { - .editor.modified { - .margin-view-overlays, - .lines-content, - .decorationsOverviewRuler { - // !important to override monaco inline styles - display: none !important; - } - } - - .diffOverviewRuler.modified { - // !important to override monaco inline styles - display: none !important; - } - } - - .is-added { - .editor.original { - .margin-view-overlays, - .lines-content, - .decorationsOverviewRuler { - // !important to override monaco inline styles - display: none !important; - } - } - - .diffOverviewRuler.original { - // !important to override monaco inline styles - display: none !important; - } - } -} - -.multi-file-editor-holder { - height: 100%; - min-height: 0; // firefox fix -} - -// Apply theme related overrides only to the white theme and none theme -.theme-white .blob-editor-container, -.theme-none .blob-editor-container { - .monaco-diff-editor { - .editor.modified { - box-shadow: none; - } - - .diagonal-fill { - display: none !important; - } - - .diffOverview { - background-color: $white; - @apply gl-border-l; - cursor: ns-resize; - } - - .diffViewport { - display: none; - } - - .char-insert { - background-color: $line-added-dark-transparent; - } - - .char-delete { - background-color: $line-removed-dark-transparent; - } - - .line-numbers { - @apply gl-text-alpha-dark-24; - } - - .view-overlays { - .line-insert { - background-color: $line-added-transparent; - } - - .line-delete { - background-color: $line-removed-transparent; - } - } - - .margin { - background-color: $white; - @apply gl-border-r; - - .line-insert { - border-right: 1px solid $line-added-dark; - } - - .line-delete { - border-right: 1px solid $line-removed-dark; - } - } - } -} - -.theme-white .multi-file-editor-holder, -.theme-none .multi-file-editor-holder { - &.is-readonly, - .editor.original { - .monaco-editor, - .monaco-editor-background, - .monaco-editor .inputarea.ime-input { - background-color: $gray-10; - } - } -} diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss deleted file mode 100644 index 05e6c6bfee3..00000000000 --- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss +++ /dev/null @@ -1,351 +0,0 @@ -// ------- -// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes -// ------- -.ide { - $bs-input-focus-border: #80bdff; - $bs-input-focus-box-shadow: rgba(0, 123, 255, 0.25); - - a:not(.btn), - .gl-button.btn-link, - .gl-button.btn-link:hover, - .gl-button.btn-link:focus, - .gl-button.btn-link:active { - color: var(--ide-link-color, $blue-600); - } - - h1, - h2, - h3, - h4, - h5, - h6, - code, - .md table:not(.code), - .md, - .md p, - .context-header > a, - input, - textarea, - .dropdown-menu li button, - .dropdown-menu-selectable li a.is-active, - .dropdown-menu-inner-title, - .ide-pipeline .top-bar, - .ide-pipeline .top-bar .controllers .controllers-buttons, - .controllers-buttons svg, - .nav-links li a.active, - .gl-tabs-nav li a.gl-tab-nav-item-active, - .md-area.is-focused { - color: var(--ide-text-color, var(--gl-text-color-default)); - } - - .badge.badge-pill { - color: var(--ide-text-color, var(--gl-icon-color-subtle)); - background-color: var(--ide-background, $badge-bg); - } - - .nav-links li:not(.md-header-toolbar) a, - .gl-tabs-nav li a, - .dropdown-menu-inner-content, - .file-row .file-row-icon svg, - .file-row:hover .file-row-icon svg { - color: var(--ide-text-color-secondary, var(--gl-text-color-subtle)); - } - - .nav-links li:not(.md-header-toolbar), - .gl-tabs-nav li { - &:hover a, - &.active a, - a:hover, - a.active { - &, - .badge.badge-pill { - color: var(--ide-text-color, $black); - border-color: var(--ide-input-border, var(--gl-border-color-strong)); - } - } - } - - .drag-handle:hover { - background-color: var(--ide-dropdown-hover-background, var(--gl-background-color-strong)); - } - - .card-header { - background-color: var(--ide-background, var(--gl-background-color-default)); - - .badge.badge-pill { - background-color: var(--ide-dropdown-hover-background, $badge-bg); - } - } - - .gl-text-subtle, - .text-secondary { - color: var(--ide-text-color-secondary, var(--gl-text-color-subtle)) !important; - } - - input[type='search']::placeholder, - input[type='text']::placeholder, - textarea::placeholder { - color: var(--ide-input-border, $gl-text-color-tertiary); - } - - .dropdown-input .dropdown-input-clear { - color: var(--ide-input-border, var(--gl-control-border-color-default)); - } - - .ide-nav-form .input-icon { - color: var(--ide-input-border, var(--gl-control-border-color-default)); - } - - code { - background-color: var(--ide-background, var(--gl-background-color-strong)); - } - - .ide-pipeline .top-bar, - .ide-terminal .top-bar { - background-color: var(--ide-background, var(--gl-background-color-subtle)); - } - - .common-note-form .md-area { - border-color: var(--ide-input-border, var(--gl-border-color-default)); - } - - .md table:not(.code) tr th { - background-color: var(--ide-highlight-background, var(--gl-background-color-strong)); - } - - &, - .card, - .common-note-form .md-area { - background-color: var(--ide-highlight-background, var(--gl-background-color-default)); - } - - .card, - .card-header, - .ide-terminal .top-bar, - .ide-pipeline .top-bar { - border-color: var(--ide-border-color, var(--gl-border-color-default)); - } - - hr { - border-color: var(--ide-border-color, var(--gl-border-color-default)); - } - - .md h1, - .md h2, - .md blockquote, - .md table:not(.code) tbody td, - .md table:not(.code) tr th, - .nav-links, - .gl-tabs-nav, - .common-note-form .md-area.is-focused .nav-links { - border-color: var(--ide-border-color-alt, var(--gl-border-color-default)); - } - - pre { - border-color: var(--ide-border-color-alt, var(--gl-border-color-default)); - - code { - background-color: var(--ide-empty-state-background, inherit); - } - } - - // highlight accents (based on navigation theme) should only apply - // in the default white theme and "none" theme. - &:not(.theme-white):not(.theme-none) { - .ide-sidebar-link.active { - color: var(--ide-highlight-accent, var(--gl-text-color-default)); - box-shadow: inset 3px 0 var(--ide-highlight-accent, var(--gl-text-color-default)); - - &.is-right { - box-shadow: inset -3px 0 var(--ide-highlight-accent, var(--gl-text-color-default)); - } - } - - .nav-links li.active a, - .nav-links li a.active { - border-color: var(--ide-highlight-accent, var(--gl-text-color-default)); - } - - .dropdown-menu .nav-links li a.active { - border-color: var(--ide-highlight-accent, var(--gl-text-color-default)); - } - - .gl-tabs-nav li a.gl-tab-nav-item-active { - box-shadow: inset 0 -2px 0 0 var(--ide-highlight-accent, var(--gl-text-color-default)); - } - - // for other themes, suppress different avatar default colors for simplicity - .avatar-container { - &, - .avatar { - color: var(--ide-text-color, var(--gl-text-color-default)); - background-color: var(--ide-highlight-background, var(--gl-background-color-default)); - border-color: var(--ide-highlight-background, var(--gl-avatar-border-color-default)); - } - } - } - - input[type='text'], - input[type='search'], - .filtered-search-box { - border-color: var(--ide-input-border, var(--gl-border-color-default)); - background: var(--ide-input-background, var(--gl-background-color-default)) !important; - } - - input[type='text']:not([disabled]):not([readonly]):focus, - .md-area.is-focused { - border-color: var(--ide-input-border, $bs-input-focus-border); - box-shadow: 0 0 0 3px var(--ide-dropdown-background, $bs-input-focus-box-shadow); - } - - input[type='text'], - input[type='search'], - .filtered-search-box, - textarea { - color: var(--ide-input-color, var(--gl-text-color-default)) !important; - } - - .filtered-search-box input[type='search'] { - border-color: transparent !important; - box-shadow: none !important; - } - - .filtered-search-token .value-container, - .filtered-search-term .value-container { - background-color: var(--ide-dropdown-hover-background, var(--gl-background-color-strong)); - color: var(--ide-text-color, var(--gl-text-color-default)); - - &:hover { - background-color: var(--ide-input-border, var(--gl-border-color-default)); - } - } - - @function calc-btn-hover-padding($original-padding, $original-border: 1px) { - @return calc(#{$original-padding + $original-border} - var(--ide-btn-hover-border-width, #{$original-border})); - } - - .btn:not(.gl-button):not(.btn-link):not([disabled]):hover { - border-width: var(--ide-btn-hover-border-width, 1px); - padding: calc-btn-hover-padding(6px) calc-btn-hover-padding(10px); - } - - .btn:not(.gl-button):not([disabled]).btn-sm:hover { - padding: calc-btn-hover-padding(4px) calc-btn-hover-padding(10px); - } - - .btn:not(.gl-button):not([disabled]).btn-block:hover { - padding: calc-btn-hover-padding(6px) 0; - } - - .btn-default:not(.gl-button), - .dropdown, - .dropdown-menu-toggle { - color: var(--ide-input-color, var(--gl-text-color-default)) !important; - border-color: var(--ide-btn-default-border, var(--gl-border-color-default)); - } - - .dropdown-menu-toggle { - border-color: var(--ide-btn-default-border, var(--gl-border-color-strong)); - background-color: var(--ide-input-background, transparent); - - &:hover, - &:focus { - background-color: var(--ide-dropdown-btn-hover-background, var(--gl-background-color-strong)) !important; - border-color: var(--ide-dropdown-btn-hover-border, var(--gl-border-color-strong)) !important; - } - } - - // todo: remove this block after all default buttons have been migrated to gl-button - .btn-default:not(.gl-button) { - background-color: var(--ide-btn-default-background, var(--gl-background-color-default)) !important; - border-color: var(--ide-btn-default-border, var(--gl-border-color-default)); - - &:hover, - &:focus { - border-color: var(--ide-btn-default-hover-border, var(--gl-border-color-default)) !important; - background-color: var(--ide-btn-default-background, var(--gl-background-color-strong)) !important; - } - - &:active, - .active { - border-color: var(--ide-btn-default-hover-border, var(--gl-border-color-default)) !important; - background-color: var(--ide-btn-default-background, var(--gl-border-color-default)) !important; - } - } - - .dropdown-menu { - color: var(--ide-text-color, var(--gl-text-color-default)); - border-color: var(--ide-background, var(--gl-border-color-default)); - background-color: var(--ide-dropdown-background, var(--gl-background-color-default)); - - .nav-links { - background-color: var(--ide-dropdown-hover-background, var(--gl-background-color-default)); - border-color: var(--ide-dropdown-hover-background, var(--gl-border-color-default)); - } - - .gl-tabs-nav { - background-color: var(--ide-dropdown-hover-background, var(--gl-background-color-default)); - box-shadow: inset 0 -2px 0 0 var(--ide-dropdown-hover-background, var(--gl-border-color-default)); - } - - .divider { - background-color: var(--ide-dropdown-hover-background, var(--gl-border-color-default)); - border-color: var(--ide-dropdown-hover-background, var(--gl-border-color-default)); - } - - li > a:not(.disable-hover):hover, - li > a:not(.disable-hover):focus, - li button:not(.disable-hover):hover, - li button:not(.disable-hover):focus, - li button.is-focused { - background-color: var(--ide-dropdown-hover-background, var(--gl-background-color-strong)); - color: var(--ide-text-color, var(--gl-text-color-default)); - } - } - - .dropdown-title, - .dropdown-input { - border-color: var(--ide-dropdown-hover-background, var(--gl-border-color-default)) !important; - } - - // todo: remove this block after all disabled buttons have been migrated to gl-button - .btn[disabled]:not(.gl-button) { - background-color: var(--ide-btn-default-background, var(--gl-background-color-subtle)) !important; - border: 1px solid var(--ide-btn-disabled-border, var(--gl-border-color-default)) !important; - color: var(--ide-btn-disabled-color, var(--gl-text-color-disabled)) !important; - } - - .md table:not(.code) tbody { - background-color: var(--ide-empty-state-background, var(--gl-background-color-default)); - } - - .animation-container { - [class^='skeleton-line-'] { - background-color: var(--ide-animation-gradient-1, var(--gl-border-color-default)); - - &::after { - background-image: linear-gradient(to right, - var(--ide-animation-gradient-1, var(--gl-border-color-default)) 0%, - var(--ide-animation-gradient-2, var(--gl-background-color-subtle)) 20%, - var(--ide-animation-gradient-1, var(--gl-border-color-default)) 40%, - var(--ide-animation-gradient-1, var(--gl-border-color-default)) 100%); - } - } - } - - .idiff.addition { - background-color: var(--ide-diff-insert, $line-added-dark); - } - - .idiff.deletion { - background-color: var(--ide-diff-remove, $line-removed-dark); - } - - ~ .popover { - box-shadow: none; - } -} - -.navbar:not(.theme-white):not(.theme-none) { - border-bottom-color: transparent; -} diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss deleted file mode 100644 index b59865949e1..00000000000 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ /dev/null @@ -1,1053 +0,0 @@ -@import 'framework/variables'; -@import 'framework/mixins'; -@import './ide_mixins'; -@import './ide_monaco_overrides'; -@import './ide_theme_overrides'; - -@import './ide_themes/dark'; -@import './ide_themes/solarized-light'; -@import './ide_themes/solarized-dark'; -@import './ide_themes/monokai'; - -// This whole file is for the legacy Web IDE -// See: https://gitlab.com/groups/gitlab-org/-/epics/7683 - -$search-list-icon-width: 18px; -$ide-activity-bar-width: 60px; -$ide-context-header-padding: 10px; -$ide-project-avatar-end: $ide-context-header-padding + 48px; -$ide-tree-padding: $gl-padding; -$ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; -$ide-commit-row-height: 32px; -$ide-commit-header-height: 48px; - -.web-ide-loader { - padding-top: 1rem; -} - -.commit-message { - @include str-truncated(250px); -} - -.ide-view { - position: relative; - margin-top: 0; - padding-bottom: $ide-statusbar-height; - color: var(--ide-text-color, var(--gl-text-color-default)); - min-height: 0; // firefox fix - - &.is-collapsed { - .ide-file-list { - max-width: 250px; - } - } - - .file-status-icon { - width: 10px; - height: 10px; - } -} - -.ide-file-list { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; // firefox fix -} - -.multi-file-loading-container { - margin-top: 10px; - padding: 10px; -} - -.multi-file-edit-pane { - display: flex; - flex-direction: column; - flex: 1; - border-left: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - border-right: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - overflow: hidden; -} - -.multi-file-tabs { - display: flex; - background-color: var(--ide-background, $gray-10); - box-shadow: inset 0 -1px var(--ide-border-color, var(--gl-border-color-default)); - - > ul { - display: flex; - overflow-x: auto; - } - - li { - display: flex; - align-items: center; - padding: $grid-size $gl-padding; - background-color: var(--ide-background-hover, $gray-50); - border-right: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - border-bottom: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - - // stylelint-disable-next-line gitlab/no-gl-class - &.active, - .gl-tab-nav-item-active { - background-color: var(--ide-highlight-background, $white); - border-bottom-color: transparent; - } - - &:not(.disabled) { - .multi-file-tab { - cursor: pointer; - } - } - - &.disabled { - .multi-file-tab-close { - cursor: default; - } - } - } - - // stylelint-disable-next-line gitlab/no-gl-class - .gl-tab-content { - padding: 0; - } - - // stylelint-disable-next-line gitlab/no-gl-class - .gl-tabs-nav { - border-width: 0; - - li { - padding: 0 !important; - background: transparent !important; - border: 0 !important; - - a { - display: flex; - align-items: center; - padding: $grid-size $gl-padding !important; - box-shadow: none !important; - font-weight: normal !important; - - background-color: var(--ide-background-hover, $gray-50); - border-right: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - border-bottom: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - - // stylelint-disable-next-line gitlab/no-gl-class - &.gl-tab-nav-item-active { - background-color: var(--ide-highlight-background, $white); - border-color: var(--ide-border-color, var(--gl-border-color-default)); - border-bottom-color: transparent; - } - - .multi-file-tab-close svg { - top: 0; - } - } - } - } -} - -.multi-file-tab { - @include str-truncated(141px); - - svg { - vertical-align: middle; - } -} - -.multi-file-tab-close { - width: 16px; - height: 16px; - padding: 0; - margin-left: $grid-size; - background: none; - border: 0; - border-radius: $gl-border-radius-base; - color: var(--ide-text-color, $gray-900); - - svg { - position: relative; - top: -2px; - } - - &:not([disabled]):hover { - background-color: var(--ide-input-border, var(--gl-border-color-default)); - } - - &:not([disabled]):focus { - background-color: var(--ide-link-color, $blue-500); - color: $white; - outline: 0; - - svg { - fill: currentColor; - } - } -} - -.multi-file-edit-pane-content { - flex: 1; - height: 0; -} - -.preview-container { - flex-grow: 1; - position: relative; - - .md-previewer { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - overflow: auto; - padding: $gl-padding; - background-color: var(--ide-empty-state-background, transparent); - - .md { - max-width: $limited-layout-width; - } - } - - .file-container { - background-color: var(--ide-empty-state-background, $gray-50); - display: flex; - height: 100%; - align-items: center; - justify-content: center; - - text-align: center; - - .file-content { - padding: $gl-padding; - max-width: 100%; - max-height: 100%; - } - - .file-info { - font-size: $label-font-size; - color: var(--ide-text-color, var(--gl-text-color-subtle)); - } - } -} - -.ide-status-bar { - color: var(--ide-text-color, var(--gl-text-color-default)); - border-top: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - padding: 2px $gl-padding-8 0; - background-color: var(--ide-footer-background, $white); - display: flex; - justify-content: space-between; - height: $ide-statusbar-height; - - position: absolute; - bottom: 0; - left: 0; - width: 100%; - - font-size: 12px; - line-height: 22px; - - * { - font-size: inherit; - } - - svg { - vertical-align: sub; - } - - .ide-status-avatar { - float: none; - margin: 0 0 1px; - } -} - -.ide-status-list { - > div + div { - padding-left: $gl-padding; - } -} - -.file-content.blob-no-preview { - a { - margin-left: auto; - margin-right: auto; - } -} - -.multi-file-commit-panel { - display: flex; - position: relative; - padding: 0; - background-color: var(--ide-background, $gray-10); - - .context-header { - width: auto; - margin-right: 0; - - > a, - > button { - text-decoration: none; - padding-top: $gl-padding-8; - padding-bottom: $gl-padding-8; - } - } - - .multi-file-commit-panel-inner { - position: relative; - display: flex; - flex-direction: column; - min-height: 100%; - min-width: 0; - width: 100%; - } - - .multi-file-commit-panel-inner-content { - display: flex; - flex: 1; - flex-direction: column; - background-color: var(--ide-highlight-background, $white); - border-left: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - border-top: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - border-top-left-radius: $border-radius-small; - min-height: 0; // firefox fix - } -} - -.multi-file-commit-panel-section { - display: flex; - flex-direction: column; - flex: 1; - max-height: 100%; - overflow: auto; -} - -.ide-commit-empty-state { - padding: 0 $gl-padding; -} - -.ide-commit-empty-state-container { - margin-top: auto; - margin-bottom: auto; -} - -.multi-file-commit-panel-header { - height: $ide-commit-header-height; - border-bottom: 1px solid var(--ide-border-color-alt, var(--gl-border-color-default)); - padding: 12px 0; -} - -.multi-file-commit-list { - flex: 1; - overflow: auto; - padding: $grid-size 0; - min-height: 60px; - - &.form-text { - margin-left: 0; - right: 0; - } -} - -.multi-file-commit-list-path { - display: flex; - align-items: center; - margin-left: -$grid-size; - margin-right: -$grid-size; - padding: $grid-size / 2 $grid-size; - border-radius: $gl-border-radius-base; - text-align: left; - cursor: pointer; - height: $ide-commit-row-height; - padding-right: 0; - - &:hover, - &:focus { - background: var(--ide-background, $gray-50); - - outline: 0; - } - - &:active { - background: var(--ide-background, $gray-100); - } - - &.is-active { - background-color: var(--ide-background, $gray-50); - } - - svg { - min-width: 16px; - vertical-align: middle; - display: inline-block; - } -} - -.multi-file-commit-list-file-path { - @include str-truncated(calc(100% - 30px)); - user-select: none; - - &:active { - text-decoration: none; - } -} - -.multi-file-commit-form { - position: relative; - background-color: var(--ide-highlight-background, $white); - border-left: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - transition: all 0.3s ease; - - > form, - > .commit-form-compact { - padding: $gl-padding 0; - margin-left: $gl-padding; - margin-right: $gl-padding; - border-top: 1px solid var(--ide-border-color-alt, var(--gl-border-color-default)); - } - - .btn { - font-size: $gl-font-size; - } - - .multi-file-commit-panel-success-message { - top: 0; - } -} - -.multi-file-commit-panel-bottom { - position: relative; -} - -.dirty-diff { - // !important need to override monaco inline style - width: 4px !important; - left: 0 !important; - - &-modified { - background-color: $blue-500; - } - - &-added { - background-color: $green-600; - } - - &-removed { - height: 0 !important; - width: 0 !important; - bottom: -2px; - border-style: solid; - border-width: 5px; - border-color: transparent transparent transparent $red-500; - - &::before { - content: ''; - position: absolute; - left: 0; - top: 0; - width: 100px; - height: 1px; - background-color: rgba($red-500, 0.5); - } - } -} - -.ide-empty-state { - display: flex; - align-items: center; - justify-content: center; - background-color: var(--ide-empty-state-background, transparent); -} - -.ide { - overflow: hidden; - flex: 1; - height: calc(100vh - var(--top-bar-height)) -} - -.ide-commit-list-container { - display: flex; - flex: 1; - flex-direction: column; - min-height: 140px; - margin-left: $gl-padding; - margin-right: $gl-padding; - - &.is-first { - border-bottom: 1px solid var(--ide-border-color-alt, var(--gl-border-color-default)); - } -} - -.ide-commit-options { - .is-disabled { - .ide-option-label { - text-decoration: line-through; - } - } - - // stylelint-disable-next-line gitlab/no-gl-class - .gl-form-radio, - .gl-form-checkbox { - color: var(--ide-text-color, var(--gl-text-color-default)); - } -} - -.ide-sidebar-link { - display: flex; - align-items: center; - justify-content: center; - position: relative; - height: 60px; - width: 100%; - padding: 0 $gl-padding; - color: var(--ide-text-color-secondary, var(--gl-text-color-subtle)); - background-color: transparent; - border: 0; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; - outline: 0; - cursor: pointer; - - svg { - margin: 0 auto; - } - - &:hover { - color: var(--ide-text-color, var(--gl-text-color-default)); - background-color: var(--ide-background-hover, $gray-50); - } - - &:focus { - color: var(--ide-text-color, var(--gl-text-color-default)); - background-color: var(--ide-background-hover, $gray-100); - } - - &.active { - // extend width over border of sidebar section - width: calc(100% + 1px); - padding-right: $gl-padding + 1px; - background-color: var(--ide-highlight-background, $white); - border-top-color: var(--ide-border-color, var(--gl-border-color-default)); - border-bottom-color: var(--ide-border-color, var(--gl-border-color-default)); - - &::after { - content: ''; - position: absolute; - right: -1px; - top: 0; - bottom: 0; - width: 1px; - background: var(--ide-highlight-background, $white); - } - } - - &.is-right { - padding-right: $gl-padding; - padding-left: $gl-padding + 1px; - - &::after { - right: auto; - left: -1px; - } - } -} - -.ide-activity-bar { - position: relative; - flex: 0 0 $ide-activity-bar-width; - z-index: 1; -} - -.ide-commit-message-field { - height: 200px; - background-color: var(--ide-highlight-background, $white); - - .md-area { - display: flex; - flex-direction: column; - height: 100%; - } - - // stylelint-disable-next-line gitlab/no-gl-class - .nav-links, - .gl-tabs-nav { - height: 30px; - } - - .form-text { - margin-top: 2px; - color: var(--ide-link-color, $blue-500); - cursor: pointer; - } -} - -.ide-commit-message-textarea-container { - position: relative; - width: 100%; - height: 100%; - overflow: hidden; - - .note-textarea { - font-family: $monospace-font; - } -} - -.ide-commit-message-highlights-container { - position: absolute; - left: 0; - top: 0; - right: -100px; - bottom: 0; - padding-right: 100px; - pointer-events: none; - z-index: 1; - - .highlights { - white-space: pre-wrap; - word-wrap: break-word; - color: transparent; - } - - mark { - margin-left: -1px; - padding: 0 2px; - border-radius: $border-radius-small; - background-color: $orange-200; - color: transparent; - opacity: 0.6; - } -} - -.ide-commit-message-textarea { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - width: 100%; - height: 100%; - z-index: 2; - background: transparent; - resize: none; -} - -.ide-tree-header { - flex: 0 0 auto; - display: flex; - align-items: center; - flex-wrap: wrap; - padding: 12px 0; - margin-left: $ide-tree-padding; - margin-right: $ide-tree-padding; - border-bottom: 1px solid var(--ide-border-color-alt, var(--gl-border-color-default)); - - svg { - color: var(--ide-text-color-secondary, var(--gl-icon-color-subtle)); - - &:focus, - &:hover { - color: var(--ide-link-color, $blue-600); - } - } - - .ide-new-btn { - margin-left: auto; - } - - button { - color: var(--ide-text-color, var(--gl-text-color-default)); - } -} - -.ide-nav-dropdown { - width: 100%; - margin-bottom: 12px; - - .dropdown-menu { - width: 385px; - max-height: initial; - } - - .dropdown-menu-toggle { - background-color: var(--ide-input-background, transparent); - - &:hover { - background-color: var(--ide-dropdown-btn-hover-background, $gray-50); - } - - svg { - vertical-align: middle; - - &, - &:hover { - color: var(--ide-text-color-secondary, var(--gl-icon-color-subtle)); - } - } - } -} - -.ide-tree-body { - overflow: auto; - padding-left: $ide-tree-padding; - padding-right: $ide-tree-padding; -} - -.commit-form-compact { - .btn { - margin-bottom: 8px; - } - - p { - margin-bottom: 0; - } -} - -// These are composite classes for use with Vue Transition -// https://vuejs.org/guide/built-ins/transition -.commit-form-slide-up-enter-active, -.commit-form-slide-up-leave-active { - position: absolute; - top: 0; - left: 0; - right: 0; - transition: all 0.3s ease; -} - -.is-full .commit-form-slide-up-enter, -.is-compact .commit-form-slide-up-leave-to { - transform: translateY(100%); -} - -.is-full .commit-form-slide-up-enter-to, -.is-compact .commit-form-slide-up-leave { - transform: translateY(0); -} - -.fade-enter, -.fade-leave-to, -.commit-form-slide-up-enter, -.commit-form-slide-up-leave-to { - opacity: 0; -} - -.ide-review-header { - flex-direction: column; - align-items: flex-start; - - .dropdown { - margin-left: auto; - } - - a { - color: var(--ide-link-color, $blue-600); - } -} - -.ide-review-sub-header { - color: var(--ide-text-color-secondary, var(--gl-text-color-subtle)); -} - -.ide-tree-changes { - display: flex; - align-items: center; - font-size: 12px; -} - -.multi-file-commit-panel-success-message { - position: absolute; - top: 61px; - left: 1px; - bottom: 0; - right: 0; - z-index: 10; - background-color: var(--ide-highlight-background, $white); - overflow: auto; - display: flex; - flex-direction: column; - justify-content: center; -} - -.ide-review-button-holder { - display: flex; - width: 100%; - align-items: center; -} - -.ide-context-header { - .avatar-container { - flex: 0 0 auto; - margin-right: 0; - } - - .ide-sidebar-project-title { - margin-left: $ide-tree-text-start - $ide-project-avatar-end; - } -} - -.ide-context-body { - min-height: 0; // firefox fix -} - -.ide-sidebar-project-title { - min-width: 0; - - .sidebar-context-title { - white-space: nowrap; - display: block; - color: var(--ide-text-color, var(--gl-text-color-default)); - } -} - -.ide-external-link { - svg { - display: none; - } - - &:hover, - &:focus { - svg { - display: inline-block; - } - } -} - -.ide-sidebar { - min-width: 60px; -} - -.ide-pipeline { - @include ide-trace-view(); - - svg { - --svg-status-bg: var(--ide-background, #{$white}); - } - - // stylelint-disable-next-line gitlab/no-gl-class - .gl-empty-state { - p { - margin: $grid-size 0; - text-align: center; - line-height: 24px; - } - - .btn, - h4 { - margin: 0; - } - } - - // stylelint-disable-next-line gitlab/no-gl-class - .gl-tab-content { - color: var(--ide-text-color, var(--gl-text-color-default)); - } -} - -.ide-pipeline-header { - min-height: 55px; - padding-left: $gl-padding; - padding-right: $gl-padding; -} - -.ide-job-item { - display: flex; - padding: 16px; - - &:not(:last-child) { - border-bottom: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - } - - .ci-status-icon { - display: flex; - justify-content: center; - min-width: 24px; - overflow: hidden; - } -} - -.ide-stage { - .card-header { - .ci-status-icon { - display: flex; - align-items: center; - } - } -} - -.ide-job-header { - min-height: 60px; - padding: 0 $gl-padding; -} - -.ide-nav-form { - // stylelint-disable-next-line gitlab/no-gl-class - .nav-links li, - .gl-tabs-nav li { - width: 50%; - padding-left: 0; - padding-right: 0; - - a { - text-align: center; - font-size: 14px; - line-height: 30px; - - // stylelint-disable-next-line gitlab/no-gl-class - &:not(.active), - &:not(.gl-tab-nav-item-active) { - background-color: var(--ide-dropdown-background, $gray-10); - } - - // stylelint-disable-next-line gitlab/no-gl-class - &.gl-tab-nav-item-active { - font-weight: bold; - } - } - } - - .dropdown-input { - padding-left: $gl-padding; - padding-right: $gl-padding; - - .input-icon { - right: auto; - left: 10px; - top: 1rem; - } - } - - .dropdown-input-field { - padding-left: $search-list-icon-width + $gl-padding; - padding-top: 2px; - padding-bottom: 2px; - } - - .tokens-container { - padding-left: $search-list-icon-width + $gl-padding; - overflow-x: hidden; - } - - .btn-link { - padding-top: $gl-padding; - padding-bottom: $gl-padding; - } -} - -.ide-search-list-current-icon { - min-width: $search-list-icon-width; -} - -.ide-search-list-empty { - height: 69px; -} - -.ide-merge-requests-dropdown-content { - max-height: 470px; -} - -.ide-merge-request-project-path { - font-size: 12px; - line-height: 16px; - color: var(--ide-text-color-secondary, var(--gl-text-color-subtle)); -} - -.ide-entry-dropdown-toggle { - padding: $gl-padding-4; - color: var(--ide-text-color, var(--gl-text-color-default)); - background-color: var(--ide-background, $gray-50); - - &:hover { - background-color: var(--ide-file-row-btn-hover-background, $gray-100); - } - - &:active, - &:focus { - color: $gray-50; - background-color: var(--ide-link-color, $blue-500); - outline: 0; - } -} - -.ide-new-btn { - display: none; - - .btn { - padding: 2px 5px; - } - - .dropdown.show .ide-entry-dropdown-toggle { - color: $gray-50; - background-color: var(--ide-link-color, $blue-500); - } -} - -.ide-file-templates { - padding: $grid-size $gl-padding; - background-color: var(--ide-background, $gray-10); - border-bottom: 1px solid var(--ide-border-color, var(--gl-border-color-default)); - - .dropdown { - min-width: 180px; - } - - .dropdown-content { - max-height: 222px; - } -} - -.ide-commit-editor-header { - height: 65px; - padding: 8px 16px; - background-color: var(--ide-background, $gray-10); - box-shadow: inset 0 -1px var(--ide-border-color, var(--gl-border-color-default)); -} - -.ide-commit-list-changed-icon { - width: $ide-commit-row-height; - height: $ide-commit-row-height; -} - -.ide-file-icon-holder { - display: flex; - align-items: center; - color: var(--ide-text-color-secondary, var(--gl-text-color-subtle)); -} - -.file-row:active { - background: var(--ide-background, $gray-100); -} - -.file-row.is-active { - background: var(--ide-background, $gray-50); -} - -.file-row:hover, -.file-row:focus { - background: var(--ide-background, $gray-50); - - .ide-new-btn { - display: block; - } - - .folder-icon { - fill: var(--ide-text-color-secondary, var(--gl-icon-color-subtle)); - } -} - -.ide-terminal { - @include ide-trace-view(); - - .terminal-wrapper { - background: $black; - @apply gl-text-disabled; - overflow: hidden; - } - - .xterm { - height: 100%; - padding: $grid-size; - } - - .xterm-viewport { - overflow-y: auto; - } -} diff --git a/app/assets/stylesheets/page_bundles/ide_themes/README.md b/app/assets/stylesheets/page_bundles/ide_themes/README.md deleted file mode 100644 index 82e89aef49b..00000000000 --- a/app/assets/stylesheets/page_bundles/ide_themes/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Web IDE Themes - -Web IDE currently supports 5 syntax highlighting themes based on themes from the user's profile preferences: - -* White -* Dark -* Monokai -* Solarized Dark -* Solarized Light - -Currently, the Web IDE supports the white theme by default, and the dark theme by the introduction of CSS -variables. - -The Web IDE automatically adds an appropriate theme class to the `ide.vue` component based on the current syntax -highlighting theme. Below are those theme classes, which come from the `gon.user_color_scheme` global setting: - -| # | Color Scheme | `gon.user_color_scheme` | Theme class | -|---|-----------------|-------------------------|-------------------------| -| 1 | White | `"white"` | `.theme-white` | -| 2 | Dark | `"dark"` | `.theme-dark` | -| 3 | Monokai | `"monokai"` | `.theme-monokai` | -| 4 | Solarized Dark | `"solarized-dark"` | `.theme-solarized-dark` | -| 5 | Solarized Light | `"solarized-light"` | `.theme-solarized-light` | -| 6 | None | `"none"` | `.theme-none` | - -## Adding New Themes (SCSS) - -To add a new theme, follow the following steps: - -1. Pick a theme from the table above, lets say **Solarized Dark**. -2. Create a new file in this folder called `_solarized_dark.scss`. -3. Copy over all the CSS variables from `_dark.scss` to `_solarized_dark.scss` and assign them your own values. - Put them under the selector `.ide.theme-solarized-dark`. -4. Import this newly created SCSS file in `ide.scss` file in the parent directory. -5. That's it! Raise a merge request with your newly added theme. - -## Modifying Monaco Themes - -Monaco themes are defined in Javascript and are stored in the `app/assets/javascripts/ide/lib/themes/` directory. -To modify any syntax highlighting colors or to synchronize the theme colors with syntax highlighting colors, you -can modify the files in that directory directly. diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss b/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss deleted file mode 100644 index 461e054ec85..00000000000 --- a/app/assets/stylesheets/page_bundles/ide_themes/_dark.scss +++ /dev/null @@ -1,60 +0,0 @@ -// ------- -// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes -// ------- -.ide.theme-dark { - --ide-border-color: #1d1f21; - --ide-border-color-alt: #333; - --ide-highlight-accent: #fff; - --ide-text-color: #ccc; - --ide-text-color-secondary: #ccc; - --ide-background: #333; - --ide-background-hover: #2d2d2d; - --ide-highlight-background: #252526; - --ide-link-color: #428fdc; - --ide-footer-background: #060606; - --ide-empty-state-background: var(--ide-border-color); - - --ide-input-border: #868686; - --ide-input-background: transparent; - --ide-input-color: #fff; - - --ide-btn-default-background: transparent; - --ide-btn-default-border: #bfbfbf; - --ide-btn-default-hover-border: #d8d8d8; - --ide-btn-default-hover-border-width: 2px; - --ide-btn-default-focus-box-shadow: 0 0 0 1px #bfbfbf; - - --ide-btn-primary-background: #1068bf; - --ide-btn-primary-border: #428fdc; - --ide-btn-primary-hover-border: #63a6e9; - --ide-btn-primary-hover-border-width: 2px; - --ide-btn-primary-focus-box-shadow: 0 0 0 1px #63a6e9; - - // Danger styles should be the same as default styles in dark theme - --ide-btn-danger-secondary-background: var(--ide-btn-default-background); - --ide-btn-danger-secondary-border: var(--ide-btn-default-border); - --ide-btn-danger-secondary-hover-border: var(--ide-btn-default-hover-border); - --ide-btn-danger-secondary-hover-border-width: var(--ide-btn-default-hover-border-width); - --ide-btn-danger-secondary-focus-box-shadow: var(--ide-btn-default-focus-box-shadow); - - --ide-btn-disabled-background: transparent; - --ide-btn-disabled-border: rgba(223, 223, 223, 0.24); - --ide-btn-disabled-hover-border: rgba(223, 223, 223, 0.24); - --ide-btn-disabled-hover-border-width: 1px; - --ide-btn-disabled-focus-box-shadow: 0 0 0 0 transparent; - --ide-btn-disabled-color: rgba(145, 145, 145, 0.48); - - --ide-dropdown-background: #404040; - --ide-dropdown-hover-background: #525252; - - --ide-dropdown-btn-hover-border: #{$gray-200}; - --ide-dropdown-btn-hover-background: #{$gray-900}; - - --ide-file-row-btn-hover-background: #{$gray-800}; - - --ide-diff-insert: rgba(155, 185, 85, 0.2); - --ide-diff-remove: rgba(255, 0, 0, 0.2); - - --ide-animation-gradient-1: #{$gray-800}; - --ide-animation-gradient-2: #{$gray-700}; -} diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_monokai.scss b/app/assets/stylesheets/page_bundles/ide_themes/_monokai.scss deleted file mode 100644 index 3bf0856b392..00000000000 --- a/app/assets/stylesheets/page_bundles/ide_themes/_monokai.scss +++ /dev/null @@ -1,60 +0,0 @@ -// ------- -// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes -// ------- -.ide.theme-monokai { - --ide-border-color: #1a1a18; - --ide-border-color-alt: #3f4237; - --ide-highlight-accent: #fff; - --ide-text-color: #ccc; - --ide-text-color-secondary: #b7b7b7; - --ide-background: #282822; - --ide-background-hover: #2d2d2d; - --ide-highlight-background: #1f1f1d; - --ide-link-color: #428fdc; - --ide-footer-background: #404338; - --ide-empty-state-background: #1a1a18; - - --ide-input-border: #7d8175; - --ide-input-background: transparent; - --ide-input-color: #fff; - - --ide-btn-default-background: transparent; - --ide-btn-default-border: #7d8175; - --ide-btn-default-hover-border: #b5bda5; - --ide-btn-default-hover-border-width: 2px; - --ide-btn-default-focus-box-shadow: 0 0 0 1px #bfbfbf; - - --ide-btn-primary-background: #1068bf; - --ide-btn-primary-border: #428fdc; - --ide-btn-primary-hover-border: #63a6e9; - --ide-btn-primary-hover-border-width: 2px; - --ide-btn-primary-focus-box-shadow: 0 0 0 1px #63a6e9; - - // Danger styles should be the same as default styles in dark theme - --ide-btn-danger-secondary-background: var(--ide-btn-default-background); - --ide-btn-danger-secondary-border: var(--ide-btn-default-border); - --ide-btn-danger-secondary-hover-border: var(--ide-btn-default-hover-border); - --ide-btn-danger-secondary-hover-border-width: var(--ide-btn-default-hover-border-width); - --ide-btn-danger-secondary-focus-box-shadow: var(--ide-btn-default-focus-box-shadow); - - --ide-btn-disabled-background: transparent; - --ide-btn-disabled-border: rgba(223, 223, 223, 0.24); - --ide-btn-disabled-hover-border: rgba(223, 223, 223, 0.24); - --ide-btn-disabled-hover-border-width: 1px; - --ide-btn-disabled-focus-box-shadow: 0 0 0 0 transparent; - --ide-btn-disabled-color: rgba(145, 145, 145, 0.48); - - --ide-dropdown-background: #36382f; - --ide-dropdown-hover-background: #404338; - - --ide-dropdown-btn-hover-border: #b5bda5; - --ide-dropdown-btn-hover-background: #3f4237; - - --ide-file-row-btn-hover-background: #404338; - - --ide-diff-insert: rgba(155, 185, 85, 0.2); - --ide-diff-remove: rgba(255, 0, 0, 0.2); - - --ide-animation-gradient-1: #404338; - --ide-animation-gradient-2: #36382f; -} diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss b/app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss deleted file mode 100644 index d7985601e8d..00000000000 --- a/app/assets/stylesheets/page_bundles/ide_themes/_solarized-dark.scss +++ /dev/null @@ -1,60 +0,0 @@ -// ------- -// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes -// ------- -.ide.theme-solarized-dark { - --ide-border-color: #002c38; - --ide-border-color-alt: var(--ide-background); - --ide-highlight-accent: #fff; - --ide-text-color: #ddd; - --ide-text-color-secondary: #ddd; - --ide-background: #004152; - --ide-background-hover: #003b4d; - --ide-highlight-background: #003240; - --ide-link-color: #73b9ff; - --ide-footer-background: var(--ide-highlight-background); - --ide-empty-state-background: var(--ide-border-color); - - --ide-input-border: #d8d8d8; - --ide-input-background: transparent; - --ide-input-color: #fff; - - --ide-btn-default-background: transparent; - --ide-btn-default-border: #d8d8d8; - --ide-btn-default-hover-border: #d8d8d8; - --ide-btn-default-hover-border-width: 2px; - --ide-btn-default-focus-box-shadow: 0 0 0 1px #d8d8d8; - - --ide-btn-primary-background: #1068bf; - --ide-btn-primary-border: #428fdc; - --ide-btn-primary-hover-border: #63a6e9; - --ide-btn-primary-hover-border-width: 2px; - --ide-btn-primary-focus-box-shadow: 0 0 0 1px #63a6e9; - - // Danger styles should be the same as default styles in dark theme - --ide-btn-danger-secondary-background: var(--ide-btn-default-background); - --ide-btn-danger-secondary-border: var(--ide-btn-default-border); - --ide-btn-danger-secondary-hover-border: var(--ide-btn-default-hover-border); - --ide-btn-danger-secondary-hover-border-width: var(--ide-btn-default-hover-border-width); - --ide-btn-danger-secondary-focus-box-shadow: var(--ide-btn-default-focus-box-shadow); - - --ide-btn-disabled-background: transparent; - --ide-btn-disabled-border: rgba(223, 223, 223, 0.24); - --ide-btn-disabled-hover-border: rgba(223, 223, 223, 0.24); - --ide-btn-disabled-hover-border-width: 1px; - --ide-btn-disabled-focus-box-shadow: transparent; - --ide-btn-disabled-color: rgba(145, 145, 145, 0.48); - - --ide-dropdown-background: #004c61; - --ide-dropdown-hover-background: #00617a; - - --ide-dropdown-btn-hover-border: #e9ecef; - --ide-dropdown-btn-hover-background: var(--ide-background-hover); - - --ide-file-row-btn-hover-background: #005a73; - - --ide-diff-insert: rgba(155, 185, 85, 0.2); - --ide-diff-remove: rgba(255, 0, 0, 0.2); - - --ide-animation-gradient-1: var(--ide-file-row-btn-hover-background); - --ide-animation-gradient-2: var(--ide-dropdown-hover-background); -} diff --git a/app/assets/stylesheets/page_bundles/ide_themes/_solarized-light.scss b/app/assets/stylesheets/page_bundles/ide_themes/_solarized-light.scss deleted file mode 100644 index 26d55dfe2d0..00000000000 --- a/app/assets/stylesheets/page_bundles/ide_themes/_solarized-light.scss +++ /dev/null @@ -1,57 +0,0 @@ -// ------- -// Please see `app/assets/stylesheets/page_bundles/ide_themes/README.md` for a guide on contributing new themes -// ------- -.ide.theme-solarized-light { - --ide-border-color: #ded7c1; - --ide-border-color-alt: #dfd7bf; - --ide-highlight-accent: #5c4e21; - --ide-text-color: #616161; - --ide-text-color-secondary: #526f76; - --ide-background: #ddd6c1; - --ide-background-hover: #d3cbb7; - --ide-highlight-background: #eee8d5; - --ide-link-color: #955800; - --ide-footer-background: #efe8d3; - --ide-empty-state-background: #fef6e1; - - --ide-input-border: #c0b9a4; - --ide-input-background: transparent; - - --ide-btn-default-background: transparent; - --ide-btn-default-border: #c0b9a4; - --ide-btn-default-hover-border: #c0b9a4; - - --ide-btn-primary-background: #b16802; - --ide-btn-primary-border: #a35f00; - --ide-btn-primary-hover-border: #955800; - --ide-btn-primary-hover-border-width: 2px; - --ide-btn-primary-focus-box-shadow: 0 0 0 1px #dd8101; - - --ide-btn-danger-secondary-background: transparent; - - --ide-btn-disabled-background: transparent; - --ide-btn-disabled-border: rgba(192, 185, 64, 0.48); - --ide-btn-disabled-hover-border: rgba(192, 185, 64, 0.48); - --ide-btn-disabled-hover-border-width: 1px; - --ide-btn-disabled-focus-box-shadow: transparent; - --ide-btn-disabled-color: rgba(82, 82, 82, 0.48); - - --ide-dropdown-background: #fef6e1; - --ide-dropdown-hover-background: #efe8d3; - - --ide-dropdown-btn-hover-border: #dfd7bf; - --ide-dropdown-btn-hover-background: #efe8d3; - - --ide-file-row-btn-hover-background: #ded6be; - - --ide-animation-gradient-1: #d3cbb3; - --ide-animation-gradient-2: #efe8d3; - - .ide-empty-state, - .ide-sidebar, - .ide-commit-empty-state { - img { - filter: sepia(1) brightness(0.7); - } - } -} diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb index c0906cb7ade..018dd43219b 100644 --- a/app/helpers/sessions_helper.rb +++ b/app/helpers/sessions_helper.rb @@ -13,9 +13,7 @@ module SessionsHelper end def remember_me_enabled? - return false if session_expire_from_init_enabled? - - Gitlab::CurrentSettings.remember_me_enabled? + Gitlab::CurrentSettings.allow_user_remember_me? end def unconfirmed_verification_email?(user) @@ -37,11 +35,4 @@ module SessionsHelper update_email_path: users_update_email_path } end - - private - - def session_expire_from_init_enabled? - Feature.enabled?(:session_expire_from_init, :instance) && - Gitlab::CurrentSettings.session_expire_from_init - end end diff --git a/app/models/active_session.rb b/app/models/active_session.rb index b7f97b1c28b..e42313451e7 100644 --- a/app/models/active_session.rb +++ b/app/models/active_session.rb @@ -76,8 +76,7 @@ class ActiveSession session_private_id = request.session.id.private_id client = Gitlab::SafeDeviceDetector.new(request.user_agent) timestamp = Time.current - key = key_name(user.id, session_private_id) - expiry = expiry_time(key) + expiry = Settings.gitlab['session_expire_delay'] * 60 active_user_session = new( ip_address: request.remote_ip, @@ -95,7 +94,7 @@ class ActiveSession Gitlab::Instrumentation::RedisClusterValidator.allow_cross_slot_commands do redis.pipelined do |pipeline| pipeline.setex( - key, + key_name(user.id, session_private_id), expiry, active_user_session.dump ) @@ -170,26 +169,6 @@ class ActiveSession "#{Gitlab::Redis::Sessions::SESSION_NAMESPACE}:#{session_id}" end - def self.expiry_time(key) - # initialize to defaults - ttl = Settings.gitlab['session_expire_delay'] * 60 - - return ttl unless Feature.enabled?(:session_expire_from_init, :instance) && - Gitlab::CurrentSettings.session_expire_from_init - - # If we're initializing a session, there won't already be a session - # Only use current session TTL if we have expire session from init enabled - Gitlab::Redis::Sessions.with do |redis| - # redis returns -2 if the key doesn't exist, -1 if no TTL - ttl_expire = redis.ttl(key) - - # for new sessions, return default ttl, otherwise, keep same ttl - ttl = ttl_expire if ttl_expire > -1 - end - - ttl - end - def self.key_name(user_id, session_id = '*') "#{Gitlab::Redis::Sessions::USER_SESSIONS_NAMESPACE}::v2:#{user_id}:#{session_id}" end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 80acdcdd11a..df1f4b97bcd 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -1165,6 +1165,18 @@ class ApplicationSetting < ApplicationRecord ChronicDuration.output(value, format: :long) if value end + def allow_user_remember_me? + return false if session_expire_from_init_enabled? + + remember_me_enabled? + end + + # check the model first, as this will be false on most instances + # only check Redis / FF if setting is enabled + def session_expire_from_init_enabled? + session_expire_from_init? && Feature.enabled?(:session_expire_from_init, :instance) + end + private def parsed_grafana_url diff --git a/app/models/user.rb b/app/models/user.rb index e3ca34be5c9..8cfa37f2116 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1220,7 +1220,7 @@ class User < ApplicationRecord # to the session cookie. When remember me is disabled this method ensures these # values aren't set. def remember_me! - super if ::Gitlab::Database.read_write? && ::Gitlab::CurrentSettings.remember_me_enabled? + super if ::Gitlab::Database.read_write? && ::Gitlab::CurrentSettings.allow_user_remember_me? end def forget_me! @@ -1246,7 +1246,7 @@ class User < ApplicationRecord # and compares to the stored value. When remember me is disabled this method ensures # the upstream comparison does not happen. def remember_me?(token, generated_at) - return false unless ::Gitlab::CurrentSettings.remember_me_enabled? + return false unless ::Gitlab::CurrentSettings.allow_user_remember_me? super end diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml index b57b845ed45..585363503db 100644 --- a/app/views/layouts/_flash.html.haml +++ b/app/views/layouts/_flash.html.haml @@ -6,6 +6,8 @@ - closable = %w[alert notice success] .flash-container.flash-container-page.sticky{ data: { testid: 'flash-container' }, class: flash_container_class } - flash.each do |key, value| + - next if key == 'timedout' # used by Warden for state tracking, not meant to be rendered + - if key == 'toast' && value -# Frontend renders toast messages as text and not as HTML. -# Since toast messages are escaped on the backend via `safe_format` we diff --git a/config/initializers/warden.rb b/config/initializers/warden.rb index 609acbb4eca..95c784a8da3 100644 --- a/config/initializers/warden.rb +++ b/config/initializers/warden.rb @@ -3,6 +3,7 @@ Rails.application.configure do |config| Warden::Manager.after_set_user(scope: :user) do |user, auth, opts| Gitlab::Auth::UniqueIpsLimiter.limit_user!(user) + Gitlab::Auth::SessionExpireFromInitEnforcer.new(auth, opts).enforce! activity = Gitlab::Auth::Activity.new(opts) @@ -24,6 +25,7 @@ Rails.application.configure do |config| Warden::Manager.after_authentication(scope: :user) do |user, auth, opts| ActiveSession.cleanup(user) ActiveSession.set_marketing_user_cookies(auth, user) if ::Gitlab.ee? && ::Gitlab.com? + Gitlab::Auth::SessionExpireFromInitEnforcer.new(auth, opts).set_login_time Gitlab::AnonymousSession.new(auth.request.remote_ip).cleanup_session_per_ip_count end diff --git a/db/migrate/20250417141103_update_default_package_metadata_purl_types_pub.rb b/db/migrate/20250417141103_update_default_package_metadata_purl_types_pub.rb new file mode 100644 index 00000000000..7ca919c457f --- /dev/null +++ b/db/migrate/20250417141103_update_default_package_metadata_purl_types_pub.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class UpdateDefaultPackageMetadataPurlTypesPub < Gitlab::Database::Migration[2.2] + milestone '18.0' + + disable_ddl_transaction! + + PARTIALLY_ENABLED_SYNC = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15].freeze + FULLY_ENABLED_SYNC = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17].freeze + + def change + change_column_default :application_settings, :package_metadata_purl_types, + from: PARTIALLY_ENABLED_SYNC, to: FULLY_ENABLED_SYNC + end +end diff --git a/db/migrate/20250417141927_add_pub_purl_type_to_application_setting.rb b/db/migrate/20250417141927_add_pub_purl_type_to_application_setting.rb new file mode 100644 index 00000000000..3fe26d507f5 --- /dev/null +++ b/db/migrate/20250417141927_add_pub_purl_type_to_application_setting.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class AddPubPurlTypeToApplicationSetting < Gitlab::Database::Migration[2.2] + milestone '18.0' + + restrict_gitlab_migration gitlab_schema: :gitlab_main + + class ApplicationSetting < MigrationRecord + end + + PUB_PURL_TYPE = 17 + + def up + ApplicationSetting.reset_column_information + application_setting = ApplicationSetting.last + return unless application_setting + + application_setting.package_metadata_purl_types |= [PUB_PURL_TYPE] + application_setting.save + end + + def down + application_setting = ApplicationSetting.last + return unless application_setting + + application_setting.package_metadata_purl_types.delete(PUB_PURL_TYPE) + application_setting.save + end +end diff --git a/db/schema_migrations/20250417141103 b/db/schema_migrations/20250417141103 new file mode 100644 index 00000000000..f91d4f78dce --- /dev/null +++ b/db/schema_migrations/20250417141103 @@ -0,0 +1 @@ +9ff093a72d79fcfb38acae568c7fcd605fe1fd14a76ed86d935e3734bdaff21d \ No newline at end of file diff --git a/db/schema_migrations/20250417141927 b/db/schema_migrations/20250417141927 new file mode 100644 index 00000000000..2a7c5891fbc --- /dev/null +++ b/db/schema_migrations/20250417141927 @@ -0,0 +1 @@ +86f791a0f34d1c5ee7e707673022030565ef95db06289f5955cc59a5674d0f6f \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 50a48307d45..bada3ef3aef 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -8957,7 +8957,7 @@ CREATE TABLE application_settings ( encrypted_product_analytics_configurator_connection_string bytea, encrypted_product_analytics_configurator_connection_string_iv bytea, silent_mode_enabled boolean DEFAULT false NOT NULL, - package_metadata_purl_types smallint[] DEFAULT '{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}'::smallint[], + package_metadata_purl_types smallint[] DEFAULT '{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,17}'::smallint[], ci_max_includes integer DEFAULT 150 NOT NULL, remember_me_enabled boolean DEFAULT true NOT NULL, diagramsnet_enabled boolean DEFAULT true NOT NULL, diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 5c3c5f71c9a..cc9dbfd3f01 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -173,9 +173,9 @@ The following metrics are available: | `gitlab_diffs_render_real_duration_seconds` | Histogram | 15.8 | Duration in seconds spent on serializing and rendering diffs on diffs batch request | `controller`, `action`, `endpoint_id` | | `gitlab_memwd_violations_total` | Counter | 15.9 | Total number of times a Ruby process violated a memory threshold | | | `gitlab_memwd_violations_handled_total` | Counter | 15.9 | Total number of times Ruby process memory violations were handled | | -| `gitlab_sli_rails_request_apdex_total` | Counter | 14.4 | Total number of request Apdex measurements. For more information, see [Rails request SLIs](../../../development/application_slis/rails_request.md) | `endpoint_id`, `feature_category`, `request_urgency` | +| `gitlab_sli_rails_request_apdex_total` | Counter | 14.4 | Total number of request Apdex measurements. | `endpoint_id`, `feature_category`, `request_urgency` | | `gitlab_sli_rails_request_apdex_success_total` | Counter | 14.4 | Total number of successful requests that met the target duration for their urgency. Divide by `gitlab_sli_rails_requests_apdex_total` to get a success ratio | `endpoint_id`, `feature_category`, `request_urgency` | -| `gitlab_sli_rails_request_error_total` | Counter | 15.7 | Total number of request error measurements. For more information, see [Rails request SLIs](../../../development/application_slis/rails_request.md) | `endpoint_id`, `feature_category`, `request_urgency`, `error` | +| `gitlab_sli_rails_request_error_total` | Counter | 15.7 | Total number of request error measurements. | `endpoint_id`, `feature_category`, `request_urgency`, `error` | | `job_register_attempts_failed_total` | Counter | 9.5 | Counts the times a runner fails to register a job | | | `job_register_attempts_total` | Counter | 9.5 | Counts the times a runner tries to register a job | | | `job_queue_duration_seconds` | Histogram | 9.5 | Request handling execution time | | diff --git a/doc/api/experiments.md b/doc/api/experiments.md index 51887e57b47..07a458fa98e 100644 --- a/doc/api/experiments.md +++ b/doc/api/experiments.md @@ -12,7 +12,7 @@ title: Experiments API {{< /details >}} -Use this API to interact with A/B experiments. For more information, see the [experiment guide](../development/experiment_guide/_index.md). This API is for internal use only. +Use this API to interact with A/B experiments. This API is for internal use only. Prerequisites: diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 32c254d6a1f..c7d46866470 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -42119,7 +42119,6 @@ Represents a workspaces agent config. | `annotations` | [`[KubernetesAnnotation!]!`](#kubernetesannotation) | Annotations to apply to Kubernetes objects. | | `clusterAgent` | [`ClusterAgent!`](#clusteragent) | Cluster agent that the workspaces agent config belongs to. | | `createdAt` | [`Time!`](#time) | Timestamp of when the workspaces agent config was created. | -| `defaultMaxHoursBeforeTermination` {{< icon name="warning-solid" >}} | [`Int!`](#int) | **Deprecated** in GitLab 17.9. Field is not used. | | `defaultResourcesPerWorkspaceContainer` {{< icon name="warning-solid" >}} | [`WorkspaceResources!`](#workspaceresources) | **Introduced** in GitLab 17.9. **Status**: Experiment. Default cpu and memory resources of the workspace container. | | `defaultRuntimeClass` | [`String!`](#string) | Default Kubernetes RuntimeClass. | | `dnsZone` | [`String!`](#string) | DNS zone where workspaces are available. | @@ -42128,7 +42127,6 @@ Represents a workspaces agent config. | `id` | [`RemoteDevelopmentWorkspacesAgentConfigID!`](#remotedevelopmentworkspacesagentconfigid) | Global ID of the workspaces agent config. | | `imagePullSecrets` {{< icon name="warning-solid" >}} | [`[ImagePullSecrets!]!`](#imagepullsecrets) | **Introduced** in GitLab 17.9. **Status**: Experiment. Kubernetes secrets to pull private images for a workspace. | | `labels` | [`[KubernetesLabel!]!`](#kuberneteslabel) | Labels to apply to Kubernetes objects. | -| `maxHoursBeforeTerminationLimit` {{< icon name="warning-solid" >}} | [`Int!`](#int) | **Deprecated** in GitLab 17.9. Field is not used. | | `maxResourcesPerWorkspace` {{< icon name="warning-solid" >}} | [`WorkspaceResources!`](#workspaceresources) | **Introduced** in GitLab 17.9. **Status**: Experiment. Maximum cpu and memory resources of the workspace. | | `networkPolicyEgress` {{< icon name="warning-solid" >}} | [`[NetworkPolicyEgress!]!`](#networkpolicyegress) | **Introduced** in GitLab 17.9. **Status**: Experiment. IP CIDR range specifications for egress destinations from a workspace. | | `networkPolicyEnabled` | [`Boolean!`](#boolean) | Whether the network policy of the workspaces agent config is enabled. | diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md index 7c4dfd3e20e..6806bdfe0b2 100644 --- a/doc/development/fe_guide/vue.md +++ b/doc/development/fe_guide/vue.md @@ -11,7 +11,6 @@ To get started with Vue, read through [their documentation](https://v2.vuejs.org What is described in the following sections can be found in these examples: -- [Web IDE](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/ide/stores) - [Security products](https://gitlab.com/gitlab-org/gitlab/-/tree/master/ee/app/assets/javascripts/vue_shared/security_reports) - [Registry](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/app/assets/javascripts/registry/stores) diff --git a/doc/user/workspace/configuration.md b/doc/user/workspace/configuration.md index a7d11dc6134..a3a4433af99 100644 --- a/doc/user/workspace/configuration.md +++ b/doc/user/workspace/configuration.md @@ -48,11 +48,13 @@ If you use AWS, you can use our OpenTofu tutorial. For more information, see {{< history >}} +- **Time before automatic termination** [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120168) in GitLab 16.0 - Support for private projects [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/124273) in GitLab 16.4. - **Git reference** and **Devfile location** [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/392382) in GitLab 16.10. - **Time before automatic termination** [renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/392382) to **Workspace automatically terminates after** in GitLab 16.10. - **Variables** [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/463514) in GitLab 17.1. - **Workspace automatically terminates after** [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/166065) in GitLab 17.6. +- **Workspace can be created from Merge Request page** [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/187320) in GitLab 18.0. {{< /history >}} @@ -67,7 +69,9 @@ Prerequisites: - You must [set up workspace infrastructure](#set-up-workspace-infrastructure). - You must have at least the Developer role for the workspace and agent projects. -To create a workspace: +{{< tabs >}} + +{{< tab title="From a project" >}} 1. On the left sidebar, select **Search or go to** and find your project. 1. Select **Edit > New workspace**. @@ -75,14 +79,33 @@ To create a workspace: 1. From the **Git reference** dropdown list, select the branch, tag, or commit hash GitLab uses to create the workspace. 1. From the **Devfile** dropdown list, select one of the following: - - [GitLab default devfile](_index.md#gitlab-default-devfile). - [Custom devfile](_index.md#custom-devfile). - 1. In **Variables**, enter the keys and values of the environment variables you want to inject into the workspace. To add a new variable, select **Add variable**. 1. Select **Create workspace**. +{{< /tab >}} + +{{< tab title="From a merge request" >}} + +1. On the left sidebar, select **Search or go to** and find your project. +1. On the left sidebar, Select **Code > Merge requests**. +1. Select **Code > Open in Workspace**. +1. From the **Cluster agent** dropdown list, select a cluster agent owned by the group the project belongs to. +1. From the **Git reference** dropdown list, select the branch, tag, or commit hash + GitLab uses to create the workspace. By default this is the source branch of the merge request. +1. From the **Devfile** dropdown list, select one of the following: + - [GitLab default devfile](_index.md#gitlab-default-devfile). + - [Custom devfile](_index.md#custom-devfile). +1. In **Variables**, enter the keys and values of the environment variables you want to inject into the workspace. + To add a new variable, select **Add variable**. +1. Select **Create workspace**. + +{{< /tab >}} + +{{< /tabs >}} + The workspace might take a few minutes to start. To open the workspace, under **Preview**, select the workspace. You also have access to the terminal and can install any necessary dependencies. diff --git a/eslint.config.mjs b/eslint.config.mjs index 6976b570363..ad9957b11ea 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -668,16 +668,6 @@ export default [ }, }, - // Web IDE config - { - files: ['app/assets/javascripts/ide/**/*.{js,vue}'], - - rules: { - // https://gitlab.com/gitlab-org/gitlab/issues/33024 - 'promise/no-nesting': 'off', - }, - }, - // Jest config jestConfig, diff --git a/lib/gitlab/auth/session_expire_from_init_enforcer.rb b/lib/gitlab/auth/session_expire_from_init_enforcer.rb new file mode 100644 index 00000000000..77fc94c9cf6 --- /dev/null +++ b/lib/gitlab/auth/session_expire_from_init_enforcer.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +# code inspired by Devise Timeoutable +# https://github.com/heartcombo/devise/blob/fec67f98f26fcd9a79072e4581b1bd40d0c7fa1d/lib/devise/hooks/timeoutable.rb#L8 +module Gitlab + module Auth + class SessionExpireFromInitEnforcer + SESSION_NAMESPACE = :sefie + + attr_reader :warden, :opts + + def self.session_expires_at(controller_session = Session.current) + warden_session = controller_session['warden.user.user.session'] + session = Gitlab::NamespacedSessionStore.new(SESSION_NAMESPACE, warden_session) + signed_in_at = session['signed_in_at'] + return 0 unless signed_in_at.present? + + signed_in_at + timeout_value + end + + def self.enabled? + Feature.enabled?(:session_expire_from_init, :instance) && + Gitlab::CurrentSettings.session_expire_from_init + end + + def self.timeout_value + Gitlab::CurrentSettings.session_expire_delay * 60 + end + + def initialize(warden, opts) + @warden = warden + @opts = opts + end + + def enabled? + self.class.enabled? + end + + def set_login_time + return unless enabled? + + set_signed_in_at + end + + def enforce! + return unless enabled? + + signed_in_at = session['signed_in_at'] + + # immediately after the setting is enabled, users may not have this value set + # we set it here so users don't have to log out and log back in to set the expiry + unless signed_in_at.present? + set_signed_in_at + return + end + + time_since_sign_in = Time.current.utc.to_i - signed_in_at + + return unless time_since_sign_in > timeout_value + + ::Devise.sign_out_all_scopes ? proxy.sign_out : proxy.sign_out(scope) + throw :warden, scope: scope, message: :timeout # rubocop:disable Cop/BanCatchThrow -- this is called from a Warden hook, which depends on throw :warden to halt and redirect + end + + private + + def set_signed_in_at + session['signed_in_at'] = Time.current.utc.to_i + end + + def timeout_value + self.class.timeout_value + end + + def proxy + @proxy ||= ::Devise::Hooks::Proxy.new(warden) + end + + def scope + opts[:scope] + end + + def session + return @session if @session + + session = warden.session(scope) + @session = Gitlab::NamespacedSessionStore.new(SESSION_NAMESPACE, session) + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bbd9f7001b1..9aba53ec3d7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1518,9 +1518,6 @@ msgstr "" msgid "%{strong_start}no%{strong_end}" msgstr "" -msgid "%{tabname} changed" -msgstr "" - msgid "%{tags} tag per image name" msgstr "" @@ -6269,9 +6266,6 @@ msgstr "" msgid "All branches" msgstr "" -msgid "All changes are committed" -msgstr "" - msgid "All child items must be confidential in order to turn on confidentiality." msgstr "" @@ -6798,9 +6792,6 @@ msgstr "" msgid "An error occurred creating the blob" msgstr "" -msgid "An error occurred creating the new branch." -msgstr "" - msgid "An error occurred deleting the group. Please refresh the page to try again." msgstr "" @@ -6969,18 +6960,12 @@ msgstr "" msgid "An error occurred while fetching the job log. The information presented below may not be accurate. Refresh the page to retrieve the latest job log." msgstr "" -msgid "An error occurred while fetching the job logs." -msgstr "" - msgid "An error occurred while fetching the job. The information presented below may not be accurate. Refresh the page to retrieve the latest job log." msgstr "" msgid "An error occurred while fetching the jobs." msgstr "" -msgid "An error occurred while fetching the latest pipeline." -msgstr "" - msgid "An error occurred while fetching the pipelines jobs." msgstr "" @@ -6996,9 +6981,6 @@ msgstr "" msgid "An error occurred while fetching. Please try again." msgstr "" -msgid "An error occurred while getting files for - %{branchId}" -msgstr "" - msgid "An error occurred while getting issue counts" msgstr "" @@ -7011,9 +6993,6 @@ msgstr "" msgid "An error occurred while loading a section of this page." msgstr "" -msgid "An error occurred while loading all the files." -msgstr "" - msgid "An error occurred while loading chart data" msgstr "" @@ -7050,33 +7029,15 @@ msgstr "" msgid "An error occurred while loading the file" msgstr "" -msgid "An error occurred while loading the file content." -msgstr "" - -msgid "An error occurred while loading the file." -msgstr "" - msgid "An error occurred while loading the file. Please try again later." msgstr "" msgid "An error occurred while loading the file. Please try again." msgstr "" -msgid "An error occurred while loading the merge request changes." -msgstr "" - -msgid "An error occurred while loading the merge request version data." -msgstr "" - -msgid "An error occurred while loading the merge request." -msgstr "" - msgid "An error occurred while loading the notification settings. Please try again." msgstr "" -msgid "An error occurred while loading the pipelines jobs." -msgstr "" - msgid "An error occurred while making the request." msgstr "" @@ -7232,21 +7193,6 @@ msgstr "" msgid "An unexpected error occurred trying to submit your comment. Please try again." msgstr "" -msgid "An unexpected error occurred while checking the project environment." -msgstr "" - -msgid "An unexpected error occurred while checking the project runners." -msgstr "" - -msgid "An unexpected error occurred while communicating with the Web Terminal." -msgstr "" - -msgid "An unexpected error occurred while starting the Web Terminal." -msgstr "" - -msgid "An unexpected error occurred while stopping the Web Terminal." -msgstr "" - msgid "An unexpected error occurred. Please try again." msgstr "" @@ -8699,9 +8645,6 @@ msgstr "" msgid "Assigned to %{assignee_name}" msgstr "" -msgid "Assigned to me" -msgstr "" - msgid "Assigned to you" msgstr "" @@ -10517,18 +10460,9 @@ msgstr "" msgid "Branch" msgstr "" -msgid "Branch %{branchName} was not found in this project's repository." -msgstr "" - msgid "Branch %{branch_name} was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgstr "" -msgid "Branch already exists" -msgstr "" - -msgid "Branch changed" -msgstr "" - msgid "Branch created." msgstr "" @@ -10556,9 +10490,6 @@ msgstr "" msgid "Branch name template" msgstr "" -msgid "Branch not loaded - %{branchId}" -msgstr "" - msgid "Branch rules" msgstr "" @@ -11842,9 +11773,6 @@ msgstr "" msgid "CICD|instance enabled" msgstr "" -msgid "CODEOWNERS rule violation" -msgstr "" - msgid "CONTRIBUTING" msgstr "" @@ -12553,12 +12481,6 @@ msgstr "" msgid "Choose a template" msgstr "" -msgid "Choose a template…" -msgstr "" - -msgid "Choose a type…" -msgstr "" - msgid "Choose an option" msgstr "" @@ -13298,9 +13220,6 @@ msgstr "" msgid "Close %{noteable}" msgstr "" -msgid "Close %{tabname}" -msgstr "" - msgid "Close design" msgstr "" @@ -14965,9 +14884,6 @@ msgstr "" msgid "Commit (when editing commit message)" msgstr "" -msgid "Commit Message" -msgstr "" - msgid "Commit SHA" msgstr "" @@ -15103,12 +15019,6 @@ msgstr "" msgid "Compare changes" msgstr "" -msgid "Compare changes with the last commit" -msgstr "" - -msgid "Compare changes with the merge request target branch" -msgstr "" - msgid "Compare revisions" msgstr "" @@ -16659,9 +16569,6 @@ msgstr "" msgid "Configure GitLab" msgstr "" -msgid "Configure GitLab runners to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}" -msgstr "" - msgid "Configure Gitaly timeouts." msgstr "" @@ -16689,9 +16596,6 @@ msgstr "" msgid "Configure Sentry integration for error tracking" msgstr "" -msgid "Configure a %{codeStart}.gitlab-webide.yml%{codeEnd} file in the %{codeStart}.gitlab%{codeEnd} directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}" -msgstr "" - msgid "Configure advanced permissions" msgstr "" @@ -16902,9 +16806,6 @@ msgstr "" msgid "Connecting" msgstr "" -msgid "Connecting to terminal sync service" -msgstr "" - msgid "Connecting…" msgstr "" @@ -18186,18 +18087,12 @@ msgstr "" msgid "Could not change HEAD: branch '%{branch}' does not exist" msgstr "" -msgid "Could not commit. An unexpected error occurred." -msgstr "" - msgid "Could not connect to FogBugz, check your URL" msgstr "" msgid "Could not connect to Sentry. Refresh the page to try again." msgstr "" -msgid "Could not connect to Web IDE file mirror service." -msgstr "" - msgid "Could not create Wiki Repository at this time. Please try again later." msgstr "" @@ -18372,12 +18267,6 @@ msgstr "" msgid "Create a new %{codeStart}.gitlab-ci.yml%{codeEnd} file at the root of the repository to get started." msgstr "" -msgid "Create a new branch" -msgstr "" - -msgid "Create a new file as there are no files yet. Afterwards, you'll be able to commit your changes." -msgstr "" - msgid "Create a new fork" msgstr "" @@ -18414,9 +18303,6 @@ msgstr "" msgid "Create commit" msgstr "" -msgid "Create commit…" -msgstr "" - msgid "Create common files more quickly, and standardize their format." msgstr "" @@ -18429,18 +18315,12 @@ msgstr "" msgid "Create custom type" msgstr "" -msgid "Create directory" -msgstr "" - msgid "Create empty repository" msgstr "" msgid "Create epic" msgstr "" -msgid "Create file" -msgstr "" - msgid "Create from" msgstr "" @@ -18477,27 +18357,15 @@ msgstr "" msgid "Create new %{name} by email" msgstr "" -msgid "Create new branch" -msgstr "" - msgid "Create new confidential %{issuableType}" msgstr "" -msgid "Create new directory" -msgstr "" - msgid "Create new domain" msgstr "" msgid "Create new emoji" msgstr "" -msgid "Create new file" -msgstr "" - -msgid "Create new file or directory" -msgstr "" - msgid "Create new label" msgstr "" @@ -18798,9 +18666,6 @@ msgstr "" msgid "Created branch '%{branch_name}' and a merge request to resolve this issue." msgstr "" -msgid "Created by me" -msgstr "" - msgid "Created by:" msgstr "" @@ -19008,9 +18873,6 @@ msgstr "" msgid "Current" msgstr "" -msgid "Current Branch" -msgstr "" - msgid "Current Project" msgstr "" @@ -22631,21 +22493,9 @@ msgstr "" msgid "Discard" msgstr "" -msgid "Discard all changes" -msgstr "" - -msgid "Discard all changes?" -msgstr "" - msgid "Discard changes" msgstr "" -msgid "Discard changes to %{path}?" -msgstr "" - -msgid "Discard draft" -msgstr "" - msgid "Discard pending review?" msgstr "" @@ -23532,9 +23382,6 @@ msgstr "" msgid "Edit file" msgstr "" -msgid "Edit files in the editor and commit changes here" -msgstr "" - msgid "Edit fork in Web IDE" msgstr "" @@ -24777,9 +24624,6 @@ msgstr "" msgid "Error fetching labels." msgstr "" -msgid "Error fetching merge requests for %{branchId}" -msgstr "" - msgid "Error fetching network graph." msgstr "" @@ -24798,12 +24642,6 @@ msgstr "" msgid "Error linking identity: Provider and Extern UID must be in the session." msgstr "" -msgid "Error loading branch data. Please try again." -msgstr "" - -msgid "Error loading branches." -msgstr "" - msgid "Error loading burndown chart data" msgstr "" @@ -24816,27 +24654,12 @@ msgstr "" msgid "Error loading iterations" msgstr "" -msgid "Error loading last commit." -msgstr "" - msgid "Error loading markdown preview" msgstr "" -msgid "Error loading merge requests." -msgstr "" - msgid "Error loading milestone tab" msgstr "" -msgid "Error loading project data. Please try again." -msgstr "" - -msgid "Error loading template types." -msgstr "" - -msgid "Error loading template." -msgstr "" - msgid "Error loading viewer" msgstr "" @@ -24891,9 +24714,6 @@ msgstr "" msgid "Error saving label update." msgstr "" -msgid "Error setting up editor. Please try again." -msgstr "" - msgid "Error tracking" msgstr "" @@ -24912,9 +24732,6 @@ msgstr "" msgid "Error uploading file. Please try again." msgstr "" -msgid "Error while loading the merge request. Please try again." -msgstr "" - msgid "Error while migrating %{upload_id}: %{error_message}" msgstr "" @@ -26480,9 +26297,6 @@ msgstr "" msgid "File suppressed by a .gitattributes entry or the file's encoding is unsupported." msgstr "" -msgid "File templates" -msgstr "" - msgid "File too large. Secure files must be less than %{limit} MB." msgstr "" @@ -30991,12 +30805,6 @@ msgstr "" msgid "IDE|Cannot open Web IDE" msgstr "" -msgid "IDE|Commit" -msgstr "" - -msgid "IDE|Commit to %{branchName} branch" -msgstr "" - msgid "IDE|Confirm" msgstr "" @@ -31006,18 +30814,12 @@ msgstr "" msgid "IDE|Could not find a callback URL entry for %{expectedCallbackUrl}." msgstr "" -msgid "IDE|Edit" -msgstr "" - msgid "IDE|Editing this application might affect the functionality of the Web IDE. Ensure the configuration meets the following conditions:" msgstr "" msgid "IDE|GitLab logo" msgstr "" -msgid "IDE|Go to project" -msgstr "" - msgid "IDE|OAuth Callback URLs" msgstr "" @@ -31033,15 +30835,6 @@ msgstr "" msgid "IDE|Restore to default" msgstr "" -msgid "IDE|Review" -msgstr "" - -msgid "IDE|Start a new merge request" -msgstr "" - -msgid "IDE|Successful commit" -msgstr "" - msgid "IDE|The %{boldStart}Confidential%{boldEnd} checkbox is cleared." msgstr "" @@ -31057,12 +30850,6 @@ msgstr "" msgid "IDE|The redirect URI path is %{codeBlockStart}%{pathFormat}%{codeBlockEnd}. An example of a valid redirect URI is %{codeBlockStart}%{example}%{codeBlockEnd}." msgstr "" -msgid "IDE|This option is disabled because you are not allowed to create merge requests in this project." -msgstr "" - -msgid "IDE|This option is disabled because you don't have write permissions for the current branch." -msgstr "" - msgid "IDs with errors: %{error_messages}." msgstr "" @@ -35330,9 +35117,6 @@ msgstr "" msgid "LFSStatus|Enabled" msgstr "" -msgid "LICENSE" -msgstr "" - msgid "Label" msgid_plural "Labels" msgstr[0] "" @@ -35558,9 +35342,6 @@ msgstr "" msgid "LastPushEvent|at" msgstr "" -msgid "Latest changes" -msgstr "" - msgid "Latest pipeline for the most recent commit on this ref" msgstr "" @@ -35624,9 +35405,6 @@ msgstr "" msgid "Learn more about Service Desk" msgstr "" -msgid "Learn more about Web Terminal" -msgstr "" - msgid "Learn more about X.509 signed commits" msgstr "" @@ -36273,9 +36051,6 @@ msgstr "" msgid "Locked" msgstr "" -msgid "Locked by %{fileLockUserName}" -msgstr "" - msgid "Locked by %{locker}" msgstr "" @@ -36399,9 +36174,6 @@ msgstr "" msgid "Make adjustments to how your GitLab instance is set up." msgstr "" -msgid "Make and review changes in the browser with the Web IDE" -msgstr "" - msgid "Make changes to the diagram" msgstr "" @@ -40238,9 +40010,6 @@ msgstr "" msgid "No branch selected" msgstr "" -msgid "No branches found" -msgstr "" - msgid "No change" msgstr "" @@ -40331,9 +40100,6 @@ msgstr "" msgid "No file selected" msgstr "" -msgid "No files" -msgstr "" - msgid "No files found." msgstr "" @@ -40397,15 +40163,9 @@ msgstr "" msgid "No memberships found" msgstr "" -msgid "No merge requests found" -msgstr "" - msgid "No merge requests match this list." msgstr "" -msgid "No messages were logged" -msgstr "" - msgid "No milestone" msgstr "" @@ -42304,9 +42064,6 @@ msgstr "" msgid "Open in Workspace" msgstr "" -msgid "Open in file view" -msgstr "" - msgid "Open in full page" msgstr "" @@ -43776,9 +43533,6 @@ msgstr "" msgid "Parse error: Unexpected input near `%{input}`." msgstr "" -msgid "Part of merge request changes" -msgstr "" - msgid "Participants" msgstr "" @@ -44786,9 +44540,6 @@ msgstr "" msgid "Pipelines|Auto DevOps" msgstr "" -msgid "Pipelines|Build with confidence" -msgstr "" - msgid "Pipelines|By revoking a trigger token you will break any processes making use of it. Are you sure?" msgstr "" @@ -44855,9 +44606,6 @@ msgstr "" msgid "Pipelines|Get started with GitLab CI/CD" msgstr "" -msgid "Pipelines|GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time consuming tasks, so you can spend more time creating." -msgstr "" - msgid "Pipelines|Go to the pipeline editor" msgstr "" @@ -50524,15 +50272,6 @@ msgstr "" msgid "Removing the bypass will cause the merge request to be blocked if any reviews have requested changes." msgstr "" -msgid "Rename file" -msgstr "" - -msgid "Rename folder" -msgstr "" - -msgid "Rename/Move" -msgstr "" - msgid "Render diagrams in your documents using PlantUML." msgstr "" @@ -51261,9 +51000,6 @@ msgstr "" msgid "Response text" msgstr "" -msgid "Restart Terminal" -msgstr "" - msgid "Restart merge train pipelines with the merged changes." msgstr "" @@ -51342,9 +51078,6 @@ msgstr "" msgid "Revert this merge request" msgstr "" -msgid "Review" -msgstr "" - msgid "Review App|View app" msgstr "" @@ -51407,12 +51140,6 @@ msgstr "" msgid "Reviewers needed" msgstr "" -msgid "Reviewing" -msgstr "" - -msgid "Reviewing (merge request !%{mergeRequestId})" -msgstr "" - msgid "Reviews" msgstr "" @@ -51527,9 +51254,6 @@ msgstr "" msgid "Run pipeline" msgstr "" -msgid "Run tests against your code live using the Web Terminal" -msgstr "" - msgid "Run this job again in order to create the necessary resources." msgstr "" @@ -53602,9 +53326,6 @@ msgstr "" msgid "Search labels" msgstr "" -msgid "Search merge requests" -msgstr "" - msgid "Search milestones" msgstr "" @@ -56295,9 +56016,6 @@ msgstr "" msgid "Select a day" msgstr "" -msgid "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes." -msgstr "" - msgid "Select a group" msgstr "" @@ -57402,9 +57120,6 @@ msgstr "" msgid "Show commits in global search results" msgstr "" -msgid "Show complete raw log" -msgstr "" - msgid "Show current status of indexing" msgstr "" @@ -58713,9 +58428,6 @@ msgstr "" msgid "Start Time" msgstr "" -msgid "Start Web Terminal" -msgstr "" - msgid "Start a %{new_merge_request} with these changes" msgstr "" @@ -58788,9 +58500,6 @@ msgstr "" msgid "Starting with GitLab 17.7, OpenSSL 3 will be used. All TLS connections require TLS 1.2 or higher. Weaker ciphers are no longer supported. Encryption must have at least of 112 bits of security. %{link_start}Learn more%{link_end}." msgstr "" -msgid "Starting..." -msgstr "" - msgid "Starts" msgstr "" @@ -59061,9 +58770,6 @@ msgstr "" msgid "Still loading..." msgstr "" -msgid "Stop Terminal" -msgstr "" - msgid "Stop impersonating" msgstr "" @@ -59073,9 +58779,6 @@ msgstr "" msgid "Stopped" msgstr "" -msgid "Stopping..." -msgstr "" - msgid "Storage" msgstr "" @@ -60201,9 +59904,6 @@ msgstr "" msgid "Terminal for environment" msgstr "" -msgid "Terminal sync service is running" -msgstr "" - msgid "Terms" msgstr "" @@ -60694,9 +60394,6 @@ msgstr "" msgid "The change requests must be completed or resolved." msgstr "" -msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git." -msgstr "" - msgid "The comment you are editing has been changed by another user. Would you like to keep your changes and overwrite the new description or discard your changes?" msgstr "" @@ -60954,9 +60651,6 @@ msgstr "" msgid "The latest pipeline for this merge request has failed." msgstr "" -msgid "The legacy Vue-based GitLab Web IDE will be removed in GitLab 18.0." -msgstr "" - msgid "The license key is invalid." msgstr "" @@ -61026,9 +60720,6 @@ msgstr "" msgid "The metric must be one of %{metrics}." msgstr "" -msgid "The name \"%{name}\" is already taken in this directory." -msgstr "" - msgid "The name of the CI/CD configuration file. A path relative to the root directory is optional (for example %{code_open}my/path/.myfile.yml%{code_close})." msgstr "" @@ -62956,9 +62647,6 @@ msgstr "" msgid "To personalize your GitLab experience, we'd like to know a bit more about you. We won't share this information with anyone." msgstr "" -msgid "To prepare for this removal, see %{linkStart}deprecations and removals%{linkEnd}." -msgstr "" - msgid "To prevent your project from being placed in a read-only state, %{manage_storage_link_start}manage your storage use%{storage_link_end} or %{support_link_start}contact support%{link_end} immediately." msgstr "" @@ -64282,9 +63970,6 @@ msgstr "" msgid "Unable to create link to vulnerability" msgstr "" -msgid "Unable to create pipeline" -msgstr "" - msgid "Unable to fetch branch list for this project." msgstr "" @@ -64866,9 +64551,6 @@ msgstr "" msgid "Uploaded date" msgstr "" -msgid "Uploading changes to terminal" -msgstr "" - msgid "Uploading..." msgstr "" @@ -66663,15 +66345,9 @@ msgstr "" msgid "View job currently using resource" msgstr "" -msgid "View jobs" -msgstr "" - msgid "View labels" msgstr "" -msgid "View log" -msgstr "" - msgid "View merge requests you're involved with from start to finish by highlighting those that Needs Attention and those you are Following." msgstr "" @@ -67598,9 +67274,6 @@ msgstr "" msgid "Web IDE and workspaces" msgstr "" -msgid "Web Terminal" -msgstr "" - msgid "Web terminal" msgstr "" @@ -67610,36 +67283,12 @@ msgstr "" msgid "WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details." msgstr "" -msgid "WebIDE|Fork project" -msgstr "" - -msgid "WebIDE|Go to fork" -msgstr "" - -msgid "WebIDE|Merge request" -msgstr "" - msgid "WebIDE|Quickly and easily edit multiple files in your project." msgstr "" msgid "WebIDE|Quickly and easily edit multiple files in your project. Press . to open" msgstr "" -msgid "WebIDE|This project does not accept unsigned commits." -msgstr "" - -msgid "WebIDE|This project does not accept unsigned commits. You can’t commit changes through the Web IDE." -msgstr "" - -msgid "WebIDE|You can’t edit files directly in this project. Fork this project and submit a merge request with your changes." -msgstr "" - -msgid "WebIDE|You can’t edit files directly in this project. Go to your fork and submit a merge request with your changes." -msgstr "" - -msgid "WebIDE|You need permission to edit files directly in this project." -msgstr "" - msgid "WebIdeOAuthCallback|Close tab" msgstr "" @@ -69679,12 +69328,6 @@ msgstr "" msgid "Workspaces|Your workspaces" msgstr "" -msgid "Would you like to create a new branch?" -msgstr "" - -msgid "Would you like to try auto-generating a branch name?" -msgstr "" - msgid "Write" msgstr "" @@ -70164,9 +69807,6 @@ msgstr "" msgid "You do not have permission to run a pipeline on this branch." msgstr "" -msgid "You do not have permission to run the Web Terminal. Please contact a project administrator." -msgstr "" - msgid "You do not have permission to set a member awaiting" msgstr "" @@ -70455,12 +70095,6 @@ msgstr "" msgid "You will first need to set up Jira Integration to use this feature." msgstr "" -msgid "You will lose all changes you've made to this file. This action cannot be undone." -msgstr "" - -msgid "You will lose all uncommitted changes you've made in this project. This action cannot be undone." -msgstr "" - msgid "You will need to update your local repositories to point to the new location." msgstr "" @@ -70760,9 +70394,6 @@ msgstr "" msgid "Your changes can be committed to %{branchName} because a merge request is open." msgstr "" -msgid "Your changes have been committed. Commit %{commitId} %{commitStats}" -msgstr "" - msgid "Your changes have been saved" msgstr "" @@ -73514,9 +73145,6 @@ msgstr "" msgid "wiki page" msgstr "" -msgid "with %{additions} additions, %{deletions} deletions." -msgstr "" - msgid "with Admin Mode" msgstr "" diff --git a/package.json b/package.json index 9613bca7d01..777be87baa1 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@gitlab/fonts": "^1.3.0", "@gitlab/query-language-rust": "0.7.0", "@gitlab/svgs": "3.128.0", - "@gitlab/ui": "113.0.0", + "@gitlab/ui": "113.2.0", "@gitlab/vue-router-vue3": "npm:vue-router@4.5.1", "@gitlab/vuex-vue3": "npm:vuex@4.1.0", "@gitlab/web-ide": "^0.0.1-dev-20250414030534", diff --git a/spec/frontend/editor/source_editor_webide_ext_spec.js b/spec/frontend/editor/source_editor_webide_ext_spec.js deleted file mode 100644 index 7e4079c17f7..00000000000 --- a/spec/frontend/editor/source_editor_webide_ext_spec.js +++ /dev/null @@ -1,54 +0,0 @@ -import { Emitter } from 'monaco-editor'; -import { setHTMLFixture } from 'helpers/fixtures'; -import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext'; -import SourceEditor from '~/editor/source_editor'; - -describe('Source Editor Web IDE Extension', () => { - let editorEl; - let editor; - let instance; - - beforeEach(() => { - setHTMLFixture('
'); - editorEl = document.getElementById('editor'); - editor = new SourceEditor(); - }); - - describe('onSetup', () => { - it.each` - width | renderSideBySide - ${'0'} | ${false} - ${'699px'} | ${false} - ${'700px'} | ${true} - `( - "correctly renders the Diff Editor when the parent element's width is $width", - ({ width, renderSideBySide }) => { - editorEl.style.width = width; - instance = editor.createDiffInstance({ el: editorEl }); - - const sideBySideSpy = jest.spyOn(instance, 'updateOptions'); - instance.use({ definition: EditorWebIdeExtension }); - - expect(sideBySideSpy).toHaveBeenCalledWith({ renderSideBySide }); - }, - ); - - it('re-renders the Diff Editor when layout of the modified editor is changed', async () => { - const emitter = new Emitter(); - editorEl.style.width = '700px'; - - instance = editor.createDiffInstance({ el: editorEl }); - instance.getModifiedEditor().onDidLayoutChange = emitter.event; - instance.use({ definition: EditorWebIdeExtension }); - - const sideBySideSpy = jest.spyOn(instance, 'updateOptions'); - await emitter.fire(); - - expect(sideBySideSpy).toHaveBeenCalledWith({ renderSideBySide: true }); - - editorEl.style.width = '0px'; - await emitter.fire(); - expect(sideBySideSpy).toHaveBeenCalledWith({ renderSideBySide: false }); - }); - }); -}); diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_connection_status_badge_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_connection_status_badge_spec.js index e0b021e64d3..2b10239c742 100644 --- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_connection_status_badge_spec.js +++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_connection_status_badge_spec.js @@ -25,11 +25,11 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_co const findReconnectTooltip = () => wrapper.findComponent(GlPopover); describe.each([ - [connectionStatus.connected, 'success', 'connected', undefined, 'Synced'], - [connectionStatus.disconnected, 'warning', 'retry', '#', 'Refresh'], - [connectionStatus.connecting, 'muted', 'spinner', undefined, 'Updating'], + [connectionStatus.connected, 'success', 'connected', undefined, 'Synced', false], + [connectionStatus.disconnected, 'warning', 'retry', '#', 'Refresh', true], + [connectionStatus.connecting, 'muted', 'spinner', undefined, 'Updating', false], // eslint-disable-next-line max-params - ])('when connection status is %s', (status, variant, icon, href, text) => { + ])('when connection status is %s', (status, variant, icon, href, text, shouldReconnect) => { beforeEach(() => { createComponent({ connectionStatus: status }); }); @@ -46,5 +46,21 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_co const tooltip = findReconnectTooltip(); expect(tooltip.props().target).toBe('status-badge-popover-id'); }); + + describe('when badge is clicked', () => { + beforeEach(() => { + findReconnectBadge().vm.$emit('click'); + }); + + if (shouldReconnect) { + it('emits reconnect event', () => { + expect(wrapper.emitted('reconnect')).toEqual([[]]); + }); + } else { + it('does not emit reconnect event', () => { + expect(wrapper.emitted('reconnect')).toBeUndefined(); + }); + } + }); }); }); diff --git a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js index 53f94fea879..c17d9d6edb3 100644 --- a/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js +++ b/spec/frontend/environments/environment_details/components/kubernetes/kubernetes_status_bar_spec.js @@ -226,7 +226,7 @@ describe('~/environments/environment_details/components/kubernetes/kubernetes_st fluxResourceStatus: { conditions: [{ status: 'True', type: 'Ready' }] }, }); - findSyncBadge().trigger('click'); + findSyncBadge().vm.$emit('click'); expect(wrapper.emitted('show-flux-resource-details')).toBeDefined(); }); diff --git a/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js index fcb6dd1141e..ed3023e94c9 100644 --- a/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js +++ b/spec/frontend/gitlab_version_check/components/gitlab_version_check_badge_spec.js @@ -27,7 +27,6 @@ describe('GitlabVersionCheckBadge', () => { unmockTracking(); }); - const findVersionCheckBadge = () => wrapper.findByTestId('check-version-badge'); const findGlBadge = () => wrapper.findComponent(GlBadge); describe('template', () => { @@ -61,8 +60,8 @@ describe('GitlabVersionCheckBadge', () => { expect(findGlBadge().attributes('href')).toBe(UPGRADE_DOCS_URL); }); - it(`tracks click_version_badge with label ${expectedUI.title} when badge is clicked`, async () => { - await findVersionCheckBadge().trigger('click'); + it(`tracks click_version_badge with label ${expectedUI.title} when badge is clicked`, () => { + findGlBadge().vm.$emit('click'); expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', { label: 'version_badge', @@ -88,8 +87,8 @@ describe('GitlabVersionCheckBadge', () => { expect(findGlBadge().attributes('href')).toBe(undefined); }); - it('does not track click_version_badge', async () => { - await findVersionCheckBadge().trigger('click'); + it('does not track click_version_badge', () => { + findGlBadge().vm.$emit('click'); expect(trackingSpy).not.toHaveBeenCalledWith(undefined, 'click_link', { label: 'version_badge', diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js deleted file mode 100644 index 95582aca8fd..00000000000 --- a/spec/frontend/ide/components/activity_bar_spec.js +++ /dev/null @@ -1,81 +0,0 @@ -import { nextTick } from 'vue'; -import { GlBadge } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import ActivityBar from '~/ide/components/activity_bar.vue'; -import { leftSidebarViews } from '~/ide/constants'; -import { createStore } from '~/ide/stores'; - -const { edit, ...VIEW_OBJECTS_WITHOUT_EDIT } = leftSidebarViews; -const MODES_WITHOUT_EDIT = Object.keys(VIEW_OBJECTS_WITHOUT_EDIT); -const MODES = Object.keys(leftSidebarViews); - -describe('IDE ActivityBar component', () => { - let wrapper; - let store; - - const findChangesBadge = () => wrapper.findComponent(GlBadge); - const findModeButton = (mode) => wrapper.findByTestId(`${mode}-mode-button`); - - const mountComponent = (state) => { - store = createStore(); - store.replaceState({ - ...store.state, - projects: { abcproject: { web_url: 'testing' } }, - currentProjectId: 'abcproject', - ...state, - }); - - wrapper = shallowMountExtended(ActivityBar, { store }); - }; - - describe('active item', () => { - // Test that mode button does not have 'active' class before click, - // and does have 'active' class after click - const testSettingActiveItem = async (mode) => { - const button = findModeButton(mode); - - expect(button.classes('active')).toBe(false); - - button.trigger('click'); - await nextTick(); - - expect(button.classes('active')).toBe(true); - }; - - it.each(MODES)('is initially set to %s mode', (mode) => { - mountComponent({ currentActivityView: leftSidebarViews[mode].name }); - - const button = findModeButton(mode); - - expect(button.classes('active')).toBe(true); - }); - - it.each(MODES_WITHOUT_EDIT)('is correctly set after clicking %s mode button', (mode) => { - mountComponent(); - - testSettingActiveItem(mode); - }); - - it('is correctly set after clicking edit mode button', () => { - // The default currentActivityView is leftSidebarViews.edit.name, - // so for the 'edit' mode, we pass a different currentActivityView. - mountComponent({ currentActivityView: leftSidebarViews.review.name }); - - testSettingActiveItem('edit'); - }); - }); - - describe('changes badge', () => { - it('is rendered when files are staged', () => { - mountComponent({ stagedFiles: [{ path: '/path/to/file' }] }); - - expect(findChangesBadge().text()).toBe('1'); - }); - - it('is not rendered when no changes are present', () => { - mountComponent(); - - expect(findChangesBadge().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/ide/components/branches/item_spec.js b/spec/frontend/ide/components/branches/item_spec.js deleted file mode 100644 index bd5dd2ff3f2..00000000000 --- a/spec/frontend/ide/components/branches/item_spec.js +++ /dev/null @@ -1,66 +0,0 @@ -import { GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Item from '~/ide/components/branches/item.vue'; -import { createRouter, EmptyRouterComponent } from '~/ide/ide_router'; -import { createStore } from '~/ide/stores'; -import Timeago from '~/vue_shared/components/time_ago_tooltip.vue'; -import { projectData } from '../../mock_data'; - -const TEST_BRANCH = { - name: 'main', - committedDate: '2018-01-05T05:50Z', -}; -const TEST_PROJECT_ID = projectData.name_with_namespace; - -describe('IDE branch item', () => { - let wrapper; - let store; - let router; - - function createComponent(props = {}) { - wrapper = shallowMount(Item, { - propsData: { - item: { ...TEST_BRANCH }, - projectId: TEST_PROJECT_ID, - isActive: false, - ...props, - }, - router, - }); - } - - beforeEach(() => { - store = createStore(); - router = createRouter(store); - router.addRoute({ - path: '/', - component: EmptyRouterComponent, - }); - }); - - describe('if not active', () => { - beforeEach(() => { - createComponent(); - }); - it('renders branch name and timeago', () => { - expect(wrapper.text()).toContain(TEST_BRANCH.name); - expect(wrapper.findComponent(Timeago).props('time')).toBe(TEST_BRANCH.committedDate); - expect(wrapper.findComponent(GlIcon).exists()).toBe(false); - }); - - it('renders link to branch', () => { - const expectedHref = router.resolve( - `/project/${TEST_PROJECT_ID}/edit/${TEST_BRANCH.name}`, - ).href; - - expect(wrapper.text()).toMatch('a'); - expect(wrapper.attributes('href')).toBe(expectedHref); - }); - }); - - it('renders icon if is not active', () => { - createComponent({ isActive: true }); - - expect(wrapper.findComponent(GlIcon).exists()).toBe(true); - }); -}); diff --git a/spec/frontend/ide/components/branches/search_list_spec.js b/spec/frontend/ide/components/branches/search_list_spec.js deleted file mode 100644 index e5a5885bbb3..00000000000 --- a/spec/frontend/ide/components/branches/search_list_spec.js +++ /dev/null @@ -1,73 +0,0 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import Item from '~/ide/components/branches/item.vue'; -import List from '~/ide/components/branches/search_list.vue'; -import { branches } from '../../mock_data'; - -Vue.use(Vuex); - -describe('IDE branches search list', () => { - let wrapper; - const fetchBranchesMock = jest.fn(); - - const createComponent = (state, currentBranchId = 'branch') => { - const fakeStore = new Vuex.Store({ - state: { - currentBranchId, - currentProjectId: 'project', - }, - modules: { - branches: { - namespaced: true, - state: { isLoading: false, branches: [], ...state }, - actions: { - fetchBranches: fetchBranchesMock, - }, - }, - }, - }); - - wrapper = shallowMount(List, { - store: fakeStore, - }); - }; - - it('calls fetch on mounted', () => { - createComponent(); - expect(fetchBranchesMock).toHaveBeenCalled(); - }); - - it('renders loading icon when `isLoading` is true', () => { - createComponent({ isLoading: true }); - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - - it('renders branches not found when search is not empty and branches list is empty', async () => { - createComponent({ branches: [] }); - wrapper.find('input[type="search"]').setValue('something'); - - await nextTick(); - expect(wrapper.text()).toContain('No branches found'); - }); - - describe('with branches', () => { - it('renders list', () => { - createComponent({ branches }); - const items = wrapper.findAllComponents(Item); - - expect(items.length).toBe(branches.length); - }); - - it('renders check next to active branch', () => { - const activeBranch = 'regular'; - createComponent({ branches }, activeBranch); - const items = wrapper.findAllComponents(Item).filter((w) => w.props('isActive')); - - expect(items.length).toBe(1); - expect(items.at(0).props('item').name).toBe(activeBranch); - }); - }); -}); diff --git a/spec/frontend/ide/components/cannot_push_code_alert_spec.js b/spec/frontend/ide/components/cannot_push_code_alert_spec.js deleted file mode 100644 index c72d8c5fccd..00000000000 --- a/spec/frontend/ide/components/cannot_push_code_alert_spec.js +++ /dev/null @@ -1,68 +0,0 @@ -import { GlButton, GlAlert } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { stubComponent } from 'helpers/stub_component'; -import CannotPushCodeAlert from '~/ide/components/cannot_push_code_alert.vue'; - -const TEST_MESSAGE = 'Hello test message!'; -const TEST_HREF = '/test/path/to/fork'; -const TEST_BUTTON_TEXT = 'Fork text'; - -describe('ide/components/cannot_push_code_alert', () => { - let wrapper; - - const createComponent = (props = {}) => { - wrapper = shallowMount(CannotPushCodeAlert, { - propsData: { - message: TEST_MESSAGE, - ...props, - }, - stubs: { - GlAlert: { - ...stubComponent(GlAlert), - template: `
`, - }, - }, - }); - }; - - const findAlert = () => wrapper.findComponent(GlAlert); - const findButtonData = () => { - const button = findAlert().findComponent(GlButton); - - if (!button.exists()) { - return null; - } - - return { - href: button.attributes('href'), - method: button.attributes('data-method'), - text: button.text(), - }; - }; - - describe('without actions', () => { - beforeEach(() => { - createComponent(); - }); - - it('shows an alert with message', () => { - expect(findAlert().props()).toMatchObject({ dismissible: false }); - expect(findAlert().text()).toBe(TEST_MESSAGE); - }); - }); - - describe.each` - action | buttonData - ${{}} | ${null} - ${{ href: TEST_HREF, text: TEST_BUTTON_TEXT }} | ${{ href: TEST_HREF, text: TEST_BUTTON_TEXT }} - ${{ href: TEST_HREF, text: TEST_BUTTON_TEXT, isForm: true }} | ${{ href: TEST_HREF, text: TEST_BUTTON_TEXT, method: 'post' }} - `('with action=$action', ({ action, buttonData }) => { - beforeEach(() => { - createComponent({ action }); - }); - - it(`show button=${JSON.stringify(buttonData)}`, () => { - expect(findButtonData()).toEqual(buttonData); - }); - }); -}); diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js deleted file mode 100644 index 3cc3aec3a72..00000000000 --- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js +++ /dev/null @@ -1,141 +0,0 @@ -import { nextTick } from 'vue'; -import { mount } from '@vue/test-utils'; -import { projectData, branches } from 'jest/ide/mock_data'; -import CommitActions from '~/ide/components/commit_sidebar/actions.vue'; -import { createStore } from '~/ide/stores'; -import { - COMMIT_TO_NEW_BRANCH, - COMMIT_TO_CURRENT_BRANCH, -} from '~/ide/stores/modules/commit/constants'; - -const ACTION_UPDATE_COMMIT_ACTION = 'commit/updateCommitAction'; - -const BRANCH_DEFAULT = 'main'; -const BRANCH_PROTECTED = 'protected/access'; -const BRANCH_PROTECTED_NO_ACCESS = 'protected/no-access'; -const BRANCH_REGULAR = 'regular'; -const BRANCH_REGULAR_NO_ACCESS = 'regular/no-access'; - -describe('IDE commit sidebar actions', () => { - let store; - let wrapper; - - const createComponent = ({ hasMR = false, currentBranchId = 'main', emptyRepo = false } = {}) => { - store.state.currentBranchId = currentBranchId; - store.state.currentProjectId = 'abcproject'; - - const proj = { ...projectData }; - proj.branches[currentBranchId] = branches.find((branch) => branch.name === currentBranchId); - proj.empty_repo = emptyRepo; - - store.state.projects = { - ...store.state.projects, - abcproject: proj, - }; - - if (hasMR) { - store.state.currentMergeRequestId = '1'; - store.state.projects[store.state.currentProjectId].mergeRequests[ - store.state.currentMergeRequestId - ] = { foo: 'bar' }; - } - - wrapper = mount(CommitActions, { store }); - return wrapper; - }; - - beforeEach(() => { - store = createStore(); - jest.spyOn(store, 'dispatch').mockImplementation(() => {}); - }); - - const findText = () => wrapper.text(); - const findRadios = () => wrapper.findAll('input[type="radio"]'); - - it('renders 2 groups', () => { - createComponent(); - - expect(findRadios()).toHaveLength(2); - }); - - it('renders current branch text', () => { - createComponent(); - - expect(findText()).toContain('Commit to main branch'); - }); - - it('hides merge request option when project merge requests are disabled', async () => { - createComponent({ hasMR: false }); - - await nextTick(); - expect(findRadios().length).toBe(2); - expect(findText()).not.toContain('Create a new branch and merge request'); - }); - - it('escapes current branch name', () => { - const injectedSrc = ''; - const escapedSrc = '<img src="x" />'; - createComponent({ currentBranchId: injectedSrc }); - - expect(wrapper.text()).not.toContain(injectedSrc); - expect(wrapper.text).not.toContain(escapedSrc); - }); - - describe('updateSelectedCommitAction', () => { - it('does not return anything if currentBranch does not exist', () => { - createComponent({ currentBranchId: null }); - - expect(store.dispatch).not.toHaveBeenCalled(); - }); - - it('is not called on mount if there is already a selected commitAction', () => { - store.state.commitAction = '1'; - createComponent({ currentBranchId: null }); - - expect(store.dispatch).not.toHaveBeenCalled(); - }); - - it('calls again after staged changes', async () => { - createComponent({ currentBranchId: null }); - - store.state.currentBranchId = 'main'; - store.state.changedFiles.push({}); - store.state.stagedFiles.push({}); - - await nextTick(); - expect(store.dispatch).toHaveBeenCalledWith(ACTION_UPDATE_COMMIT_ACTION, expect.anything()); - }); - - it.each` - input | expectedOption - ${{ currentBranchId: BRANCH_DEFAULT }} | ${COMMIT_TO_NEW_BRANCH} - ${{ currentBranchId: BRANCH_DEFAULT, emptyRepo: true }} | ${COMMIT_TO_CURRENT_BRANCH} - ${{ currentBranchId: BRANCH_PROTECTED, hasMR: true }} | ${COMMIT_TO_CURRENT_BRANCH} - ${{ currentBranchId: BRANCH_PROTECTED, hasMR: false }} | ${COMMIT_TO_CURRENT_BRANCH} - ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: true }} | ${COMMIT_TO_NEW_BRANCH} - ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: false }} | ${COMMIT_TO_NEW_BRANCH} - ${{ currentBranchId: BRANCH_REGULAR, hasMR: true }} | ${COMMIT_TO_CURRENT_BRANCH} - ${{ currentBranchId: BRANCH_REGULAR, hasMR: false }} | ${COMMIT_TO_CURRENT_BRANCH} - ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: true }} | ${COMMIT_TO_NEW_BRANCH} - ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: false }} | ${COMMIT_TO_NEW_BRANCH} - `( - 'with $input, it dispatches update commit action with $expectedOption', - ({ input, expectedOption }) => { - createComponent(input); - - expect(store.dispatch.mock.calls).toEqual([[ACTION_UPDATE_COMMIT_ACTION, expectedOption]]); - }, - ); - }); - - describe('when empty project', () => { - beforeEach(() => { - createComponent({ emptyRepo: true }); - }); - - it('only renders commit to current branch', () => { - expect(findRadios().length).toBe(1); - expect(findText()).toContain('Commit to main branch'); - }); - }); -}); diff --git a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js b/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js deleted file mode 100644 index 2313c177bb6..00000000000 --- a/spec/frontend/ide/components/commit_sidebar/editor_header_spec.js +++ /dev/null @@ -1,74 +0,0 @@ -import { GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import EditorHeader from '~/ide/components/commit_sidebar/editor_header.vue'; -import { stubComponent } from 'helpers/stub_component'; -import { createStore } from '~/ide/stores'; -import { file } from '../../helpers'; - -Vue.use(Vuex); - -const TEST_FILE_PATH = 'test/file/path'; - -describe('IDE commit editor header', () => { - let wrapper; - let store; - const showMock = jest.fn(); - - const createComponent = (fileProps = {}) => { - wrapper = shallowMount(EditorHeader, { - store, - propsData: { - activeFile: { - ...file(TEST_FILE_PATH), - staged: true, - ...fileProps, - }, - }, - stubs: { - GlModal: stubComponent(GlModal, { - methods: { show: showMock }, - }), - }, - }); - }; - - const findDiscardModal = () => wrapper.findComponent({ ref: 'discardModal' }); - const findDiscardButton = () => wrapper.findComponent({ ref: 'discardButton' }); - - it.each` - fileProps | shouldExist - ${{ staged: false, changed: false }} | ${false} - ${{ staged: true, changed: false }} | ${true} - ${{ staged: false, changed: true }} | ${true} - ${{ staged: true, changed: true }} | ${true} - `('with $fileProps, show discard button is $shouldExist', ({ fileProps, shouldExist }) => { - createComponent(fileProps); - - expect(findDiscardButton().exists()).toBe(shouldExist); - }); - - describe('discard button', () => { - it('opens a dialog confirming discard', () => { - createComponent(); - findDiscardButton().vm.$emit('click'); - - expect(showMock).toHaveBeenCalled(); - }); - - it('calls discardFileChanges if dialog result is confirmed', () => { - store = createStore(); - jest.spyOn(store, 'dispatch').mockImplementation(); - - createComponent(); - - expect(store.dispatch).not.toHaveBeenCalled(); - - findDiscardModal().vm.$emit('primary'); - - expect(store.dispatch).toHaveBeenCalledWith('discardFileChanges', TEST_FILE_PATH); - }); - }); -}); diff --git a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js deleted file mode 100644 index 4a6aafe42ae..00000000000 --- a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js +++ /dev/null @@ -1,17 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import EmptyState from '~/ide/components/commit_sidebar/empty_state.vue'; -import { createStore } from '~/ide/stores'; - -describe('IDE commit panel EmptyState component', () => { - let wrapper; - - beforeEach(() => { - const store = createStore(); - store.state.noChangesStateSvgPath = 'no-changes'; - wrapper = shallowMount(EmptyState, { store }); - }); - - it('renders no changes text when last commit message is empty', () => { - expect(wrapper.find('h4').text()).toBe('No changes'); - }); -}); diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js deleted file mode 100644 index 534cb2598de..00000000000 --- a/spec/frontend/ide/components/commit_sidebar/form_spec.js +++ /dev/null @@ -1,357 +0,0 @@ -import { GlModal } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import { stubComponent } from 'helpers/stub_component'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import waitForPromises from 'helpers/wait_for_promises'; -import { projectData } from 'jest/ide/mock_data'; -import CommitForm from '~/ide/components/commit_sidebar/form.vue'; -import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue'; -import { leftSidebarViews } from '~/ide/constants'; -import { - createCodeownersCommitError, - createUnexpectedCommitError, - createBranchChangedCommitError, - branchAlreadyExistsCommitError, -} from '~/ide/lib/errors'; -import { MSG_CANNOT_PUSH_CODE } from '~/ide/messages'; -import { createStore } from '~/ide/stores'; -import { COMMIT_TO_NEW_BRANCH } from '~/ide/stores/modules/commit/constants'; - -describe('IDE commit form', () => { - let wrapper; - let store; - const showModalSpy = jest.fn(); - - const createComponent = () => { - wrapper = shallowMount(CommitForm, { - store, - directives: { - GlTooltip: createMockDirective('gl-tooltip'), - }, - stubs: { - GlModal: stubComponent(GlModal, { - methods: { - show: showModalSpy, - }, - }), - }, - }); - }; - - const setLastCommitMessage = (msg) => { - store.state.lastCommitMsg = msg; - }; - const goToCommitView = () => { - store.state.currentActivityView = leftSidebarViews.commit.name; - }; - const goToEditView = () => { - store.state.currentActivityView = leftSidebarViews.edit.name; - }; - const findBeginCommitButton = () => wrapper.find('[data-testid="begin-commit-button"]'); - const findBeginCommitButtonTooltip = () => - wrapper.find('[data-testid="begin-commit-button-tooltip"]'); - const findBeginCommitButtonData = () => ({ - disabled: findBeginCommitButton().props('disabled'), - tooltip: getBinding(findBeginCommitButtonTooltip().element, 'gl-tooltip').value.title, - }); - const findCommitButton = () => wrapper.find('[data-testid="commit-button"]'); - const findCommitButtonTooltip = () => wrapper.find('[data-testid="commit-button-tooltip"]'); - const findCommitButtonData = () => ({ - disabled: findCommitButton().props('disabled'), - tooltip: getBinding(findCommitButtonTooltip().element, 'gl-tooltip').value.title, - }); - const findForm = () => wrapper.find('form'); - const findModal = () => wrapper.findComponent(GlModal); - const submitForm = () => findForm().trigger('submit'); - const findCommitMessageInput = () => wrapper.findComponent(CommitMessageField); - const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val); - const findDiscardDraftButton = () => wrapper.find('[data-testid="discard-draft"]'); - - beforeEach(() => { - store = createStore(); - store.state.stagedFiles.push('test'); - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'main'; - store.state.projects = { - ...store.state.projects, - abcproject: { - ...projectData, - userPermissions: { pushCode: true }, - }, - }; - }); - - // Notes: - // - When there are no changes, there is no commit button so there's nothing to test :) - describe.each` - desc | stagedFiles | userPermissions | viewFn | buttonFn | disabled | tooltip - ${'when there are no changes'} | ${[]} | ${{ pushCode: true }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${''} - ${'when there are changes'} | ${['test']} | ${{ pushCode: true }} | ${goToEditView} | ${findBeginCommitButtonData} | ${false} | ${''} - ${'when there are changes'} | ${['test']} | ${{ pushCode: true }} | ${goToCommitView} | ${findCommitButtonData} | ${false} | ${''} - ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${MSG_CANNOT_PUSH_CODE} - ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToCommitView} | ${findCommitButtonData} | ${true} | ${MSG_CANNOT_PUSH_CODE} - `('$desc', ({ stagedFiles, userPermissions, viewFn, buttonFn, disabled, tooltip }) => { - beforeEach(() => { - store.state.stagedFiles = stagedFiles; - store.state.projects.abcproject.userPermissions = userPermissions; - - createComponent(); - }); - - it(`at view=${viewFn.name}, ${buttonFn.name} has disabled=${disabled} tooltip=${tooltip}`, async () => { - viewFn(); - - await nextTick(); - - expect(buttonFn()).toEqual({ - disabled, - tooltip, - }); - }); - }); - - describe('on edit tab', () => { - beforeEach(async () => { - // Test that we react to switching to compact view. - goToCommitView(); - - createComponent(); - - goToEditView(); - - await nextTick(); - }); - - it('renders commit button in compact mode', () => { - expect(findBeginCommitButton().exists()).toBe(true); - expect(findBeginCommitButton().text()).toBe('Create commit…'); - }); - - it('does not render form', () => { - expect(findForm().exists()).toBe(false); - }); - - it('renders overview text', () => { - expect(wrapper.find('p').text()).toBe('1 changed file'); - }); - - it('when begin commit button is clicked, shows form', async () => { - findBeginCommitButton().vm.$emit('click'); - - await nextTick(); - - expect(findForm().exists()).toBe(true); - }); - - it('when begin commit button is clicked, sets activity view', async () => { - findBeginCommitButton().vm.$emit('click'); - - await nextTick(); - - expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name); - }); - - it('collapses if lastCommitMsg is set to empty and current view is not commit view', async () => { - // Test that it expands when lastCommitMsg is set - setLastCommitMessage('test'); - goToEditView(); - - await nextTick(); - - expect(findForm().exists()).toBe(true); - - // Now test that it collapses when lastCommitMsg is cleared - setLastCommitMessage(''); - - await nextTick(); - - expect(findForm().exists()).toBe(false); - }); - }); - - describe('on commit tab when window height is less than MAX_WINDOW_HEIGHT', () => { - let oldHeight; - - beforeEach(async () => { - oldHeight = window.innerHeight; - window.innerHeight = 700; - - createComponent(); - - goToCommitView(); - - await nextTick(); - }); - - afterEach(() => { - window.innerHeight = oldHeight; - }); - - it('stays collapsed if changes are added or removed', async () => { - expect(findForm().exists()).toBe(false); - - store.state.stagedFiles = []; - await nextTick(); - - expect(findForm().exists()).toBe(false); - - store.state.stagedFiles.push('test'); - await nextTick(); - - expect(findForm().exists()).toBe(false); - }); - }); - - describe('on commit tab', () => { - beforeEach(async () => { - // Test that the component reacts to switching to full view - goToEditView(); - - createComponent(); - - goToCommitView(); - - await nextTick(); - }); - - it('shows form', () => { - expect(findForm().exists()).toBe(true); - }); - - it('hides begin commit button', () => { - expect(findBeginCommitButton().exists()).toBe(false); - }); - - describe('when no changed files', () => { - beforeEach(async () => { - store.state.stagedFiles = []; - await nextTick(); - }); - - it('hides form', () => { - expect(findForm().exists()).toBe(false); - }); - - it('expands again when staged files are added', async () => { - store.state.stagedFiles.push('test'); - await nextTick(); - - expect(findForm().exists()).toBe(true); - }); - }); - - it('updates commitMessage in store on input', async () => { - setCommitMessageInput('testing commit message'); - - await nextTick(); - - expect(store.state.commit.commitMessage).toBe('testing commit message'); - }); - - describe('discard draft button', () => { - it('hidden when commitMessage is empty', () => { - expect(findDiscardDraftButton().exists()).toBe(false); - }); - - it('resets commitMessage when clicking discard button', async () => { - setCommitMessageInput('testing commit message'); - - await nextTick(); - - expect(findCommitMessageInput().props('text')).toBe('testing commit message'); - - // Test that commitMessage is cleared on click - findDiscardDraftButton().vm.$emit('click'); - - await nextTick(); - - expect(findCommitMessageInput().props('text')).toBe(''); - }); - }); - - describe('when submitting', () => { - beforeEach(async () => { - goToEditView(); - - createComponent(); - - goToCommitView(); - - await nextTick(); - - setCommitMessageInput('testing commit message'); - - await nextTick(); - - jest.spyOn(store, 'dispatch').mockResolvedValue(); - }); - - it('when submitting form, commits changes', () => { - submitForm(); - - expect(store.dispatch).toHaveBeenCalledWith('commit/commitChanges', undefined); - }); - - it('when cannot push code, submitting does nothing', async () => { - store.state.projects.abcproject.userPermissions.pushCode = false; - await nextTick(); - - submitForm(); - - expect(store.dispatch).not.toHaveBeenCalled(); - }); - - it.each` - createError | props - ${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }} - ${createUnexpectedCommitError} | ${{ actionPrimary: null }} - `('opens error modal if commitError with $error', async ({ createError, props }) => { - const error = createError(); - store.state.commit.commitError = error; - - await nextTick(); - - expect(showModalSpy).toHaveBeenCalled(); - expect(findModal().props()).toMatchObject({ - actionCancel: { text: 'Cancel' }, - ...props, - }); - // Because of the legacy 'mountComponent' approach here, the only way to - // test the text of the modal is by viewing the content of the modal added to the document. - expect(findModal().html()).toContain(error.messageHTML); - }); - }); - - describe('with error modal with primary', () => { - beforeEach(() => { - jest.spyOn(store, 'dispatch').mockResolvedValue(); - }); - - const commitActions = [ - ['commit/updateCommitAction', COMMIT_TO_NEW_BRANCH], - ['commit/commitChanges'], - ]; - - it.each` - commitError | expectedActions - ${createCodeownersCommitError} | ${commitActions} - ${createBranchChangedCommitError} | ${commitActions} - ${branchAlreadyExistsCommitError} | ${[['commit/addSuffixToBranchName'], ...commitActions]} - `( - 'updates commit action and commits for error: $commitError', - async ({ commitError, expectedActions }) => { - store.state.commit.commitError = commitError('test message'); - - await nextTick(); - - findModal().vm.$emit('ok'); - - await waitForPromises(); - - expect(store.dispatch.mock.calls).toEqual(expectedActions); - }, - ); - }); - }); -}); diff --git a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js b/spec/frontend/ide/components/commit_sidebar/list_item_spec.js deleted file mode 100644 index 27d9bff5b80..00000000000 --- a/spec/frontend/ide/components/commit_sidebar/list_item_spec.js +++ /dev/null @@ -1,139 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { GlIcon } from '@gitlab/ui'; -import { nextTick } from 'vue'; -import { trimText } from 'helpers/text_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import ListItem from '~/ide/components/commit_sidebar/list_item.vue'; -import { describeSkipVue3, SkipReason } from 'helpers/vue3_conditional'; -import { createRouter } from '~/ide/ide_router'; -import { createStore } from '~/ide/stores'; -import { file } from '../../helpers'; - -const skipReason = new SkipReason({ - name: 'Multi-file editor commit sidebar list item', - reason: 'Legacy WebIDE is due for deletion', - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/508949', -}); -describeSkipVue3(skipReason, () => { - let wrapper; - let testFile; - let findPathEl; - let store; - let router; - - beforeEach(() => { - store = createStore(); - jest.spyOn(store, 'dispatch'); - - router = createRouter(store); - - testFile = file('test-file'); - testFile.prevName = ''; - - store.state.entries[testFile.path] = testFile; - - wrapper = mount(ListItem, { - store, - propsData: { - file: testFile, - activeFileKey: `staged-${testFile.key}`, - }, - }); - - findPathEl = wrapper.find('.multi-file-commit-list-path'); - }); - - const findPathText = () => trimText(findPathEl.text()); - - it('renders file path', () => { - expect(findPathText()).toContain(testFile.path); - }); - - it('correctly renders renamed entries', async () => { - testFile.prevName = 'Old name'; - await nextTick(); - - expect(findPathText()).toEqual(`Old name → ${testFile.name}`); - }); - - it('correctly renders entry, the name of which did not change after rename (as within a folder)', async () => { - testFile.prevName = testFile.name; - await nextTick(); - - expect(findPathText()).toEqual(testFile.name); - }); - - it('opens a closed file in the editor when clicking the file path', async () => { - jest.spyOn(router, 'push').mockImplementation(() => {}); - - await findPathEl.trigger('click'); - - expect(store.dispatch).toHaveBeenCalledWith('openPendingTab', expect.anything()); - expect(router.push).toHaveBeenCalled(); - }); - - it('calls updateViewer with diff when clicking file', async () => { - jest.spyOn(router, 'push').mockImplementation(() => {}); - - await findPathEl.trigger('click'); - await waitForPromises(); - - expect(store.dispatch).toHaveBeenCalledWith('updateViewer', 'diff'); - }); - - describe('icon name', () => { - const getIconName = () => wrapper.findComponent(GlIcon).props('name'); - - it('is modified when not a tempFile', () => { - expect(getIconName()).toBe('file-modified'); - }); - - it('is addition when is a tempFile', async () => { - testFile.tempFile = true; - await nextTick(); - - expect(getIconName()).toBe('file-addition'); - }); - - it('is deletion when is deleted', async () => { - testFile.deleted = true; - await nextTick(); - - expect(getIconName()).toBe('file-deletion'); - }); - }); - - describe('icon class', () => { - const getIconClass = () => wrapper.findComponent(GlIcon).classes(); - - it('is modified when not a tempFile', () => { - expect(getIconClass()).toContain('ide-file-modified'); - }); - - it('is addition when is a tempFile', async () => { - testFile.tempFile = true; - await nextTick(); - - expect(getIconClass()).toContain('ide-file-addition'); - }); - - it('returns deletion when is deleted', async () => { - testFile.deleted = true; - await nextTick(); - - expect(getIconClass()).toContain('ide-file-deletion'); - }); - }); - - describe('is active', () => { - it('does not add active class when dont keys match', () => { - expect(wrapper.find('.is-active').exists()).toBe(false); - }); - - it('adds active class when keys match', async () => { - await wrapper.setProps({ keyPrefix: 'staged' }); - - expect(wrapper.find('.is-active').exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js deleted file mode 100644 index c0b0cb0b732..00000000000 --- a/spec/frontend/ide/components/commit_sidebar/list_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import CommitSidebarList from '~/ide/components/commit_sidebar/list.vue'; -import ListItem from '~/ide/components/commit_sidebar/list_item.vue'; -import { file } from '../../helpers'; - -describe('Multi-file editor commit sidebar list', () => { - let wrapper; - - const mountComponent = ({ fileList }) => - shallowMount(CommitSidebarList, { - propsData: { - title: 'Staged', - fileList, - action: 'stageAllChanges', - actionBtnText: 'stage all', - actionBtnIcon: 'history', - activeFileKey: 'staged-testing', - keyPrefix: 'staged', - }, - }); - - describe('with a list of files', () => { - beforeEach(() => { - const f = file('file name'); - f.changed = true; - wrapper = mountComponent({ fileList: [f] }); - }); - - it('renders list', () => { - expect(wrapper.findAllComponents(ListItem)).toHaveLength(1); - }); - }); - - describe('with empty files array', () => { - beforeEach(() => { - wrapper = mountComponent({ fileList: [] }); - }); - - it('renders no changes text', () => { - expect(wrapper.text()).toContain('No changes'); - }); - }); -}); diff --git a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js deleted file mode 100644 index 48299354aef..00000000000 --- a/spec/frontend/ide/components/commit_sidebar/message_field_spec.js +++ /dev/null @@ -1,120 +0,0 @@ -import { nextTick } from 'vue'; -import { mount } from '@vue/test-utils'; -import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue'; -import { describeSkipVue3, SkipReason } from 'helpers/vue3_conditional'; - -const skipReason = new SkipReason({ - name: 'IDE commit message field', - reason: 'Legacy WebIDE is due for deletion', - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/508949', -}); -describeSkipVue3(skipReason, () => { - let wrapper; - - beforeEach(() => { - wrapper = mount(CommitMessageField, { - propsData: { - text: '', - placeholder: 'testing', - }, - attachTo: document.body, - }); - }); - - const findMessage = () => wrapper.find('textarea'); - const findHighlights = () => wrapper.findAll('.highlights span'); - const findMarks = () => wrapper.findAll('mark'); - - it('adds is-focused class on focus', async () => { - await findMessage().trigger('focus'); - - expect(wrapper.find('.is-focused').exists()).toBe(true); - }); - - it('removed is-focused class on blur', async () => { - await findMessage().trigger('focus'); - - expect(wrapper.find('.is-focused').exists()).toBe(true); - - await findMessage().trigger('blur'); - - expect(wrapper.find('.is-focused').exists()).toBe(false); - }); - - it('emits input event on input', async () => { - await findMessage().setValue('testing'); - - expect(wrapper.emitted('input')[0]).toStrictEqual(['testing']); - }); - - describe('highlights', () => { - describe('subject line', () => { - it('does not highlight less than 50 characters', async () => { - await wrapper.setProps({ text: 'text less than 50 chars' }); - - expect(findHighlights()).toHaveLength(1); - expect(findHighlights().at(0).text()).toContain('text less than 50 chars'); - - expect(findMarks()).toHaveLength(1); - expect(findMarks().at(0).isVisible()).toBe(false); - }); - - it('highlights characters over 50 length', async () => { - await wrapper.setProps({ - text: 'text less than 50 chars that should not highlighted. text more than 50 should be highlighted', - }); - - expect(findHighlights()).toHaveLength(1); - expect(findHighlights().at(0).text()).toContain( - 'text less than 50 chars that should not highlighte', - ); - - expect(findMarks()).toHaveLength(1); - expect(findMarks().at(0).isVisible()).toBe(true); - expect(findMarks().at(0).text()).toBe('d. text more than 50 should be highlighted'); - }); - }); - - describe('body text', () => { - it('does not highlight body text less tan 72 characters', async () => { - await wrapper.setProps({ text: 'subject line\nbody content' }); - - expect(findHighlights()).toHaveLength(2); - expect(findMarks().at(1).isVisible()).toBe(false); - }); - - it('highlights body text more than 72 characters', async () => { - await wrapper.setProps({ - text: 'subject line\nbody content that will be highlighted when it is more than 72 characters in length', - }); - - expect(findHighlights()).toHaveLength(2); - expect(findMarks().at(1).isVisible()).toBe(true); - expect(findMarks().at(1).text()).toBe('in length'); - }); - - it('highlights body text & subject line', async () => { - await wrapper.setProps({ - text: 'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length', - }); - - expect(findHighlights()).toHaveLength(2); - expect(findMarks()).toHaveLength(2); - - expect(findMarks().at(0).text()).toContain('d'); - expect(findMarks().at(1).text()).toBe('in length'); - }); - }); - }); - - describe('scrolling textarea', () => { - it('updates transform of highlights', async () => { - await wrapper.setProps({ text: 'subject line\n\n\n\n\n\n\n\n\n\n\nbody content' }); - - findMessage().element.scrollTo(0, 50); - await nextTick(); - - expect(wrapper.find('.highlights').element.style.transform).toBe('translate3d(0, -50px, 0)'); - }); - }); -}); diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js deleted file mode 100644 index 26c709e6951..00000000000 --- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js +++ /dev/null @@ -1,102 +0,0 @@ -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { GlFormCheckbox } from '@gitlab/ui'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue'; -import { createStoreOptions } from '~/ide/stores'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; - -Vue.use(Vuex); - -describe('NewMergeRequestOption component', () => { - let store; - let wrapper; - - const findCheckbox = () => wrapper.findComponent(GlFormCheckbox); - const findFieldset = () => wrapper.findByTestId('new-merge-request-fieldset'); - const findTooltip = () => getBinding(findFieldset().element, 'gl-tooltip'); - - const createComponent = ({ - shouldHideNewMrOption = false, - shouldDisableNewMrOption = false, - shouldCreateMR = false, - } = {}) => { - const storeOptions = createStoreOptions(); - - store = new Vuex.Store({ - ...storeOptions, - modules: { - ...storeOptions.modules, - commit: { - ...storeOptions.modules.commit, - getters: { - shouldHideNewMrOption: () => shouldHideNewMrOption, - shouldDisableNewMrOption: () => shouldDisableNewMrOption, - shouldCreateMR: () => shouldCreateMR, - }, - }, - }, - }); - - wrapper = shallowMountExtended(NewMergeRequestOption, { - store, - directives: { - GlTooltip: createMockDirective('gl-tooltip'), - }, - }); - }; - - describe('when the `shouldHideNewMrOption` getter returns false', () => { - beforeEach(() => { - createComponent(); - jest.spyOn(store, 'dispatch').mockImplementation(); - }); - - it('renders an enabled new MR checkbox', () => { - expect(findCheckbox().attributes('disabled')).toBeUndefined(); - }); - - it("doesn't add `is-disabled` class to the fieldset", () => { - expect(findFieldset().classes()).not.toContain('is-disabled'); - }); - - it('dispatches toggleShouldCreateMR when clicking checkbox', () => { - findCheckbox().vm.$emit('change'); - - expect(store.dispatch).toHaveBeenCalledWith('commit/toggleShouldCreateMR', undefined); - }); - - describe('when user cannot create an MR', () => { - beforeEach(() => { - createComponent({ - shouldDisableNewMrOption: true, - }); - }); - - it('disables the new MR checkbox', () => { - expect(findCheckbox().attributes('disabled')).toBeDefined(); - }); - - it('adds `is-disabled` class to the fieldset', () => { - expect(findFieldset().classes()).toContain('is-disabled'); - }); - - it('shows a tooltip', () => { - expect(findTooltip().value).toBe(wrapper.vm.$options.i18n.tooltipText); - }); - }); - }); - - describe('when the `shouldHideNewMrOption` getter returns true', () => { - beforeEach(() => { - createComponent({ - shouldHideNewMrOption: true, - }); - }); - - it("doesn't render the new MR checkbox", () => { - expect(findCheckbox().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js deleted file mode 100644 index cdf14056523..00000000000 --- a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js +++ /dev/null @@ -1,112 +0,0 @@ -import { GlFormRadioGroup } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import RadioGroup from '~/ide/components/commit_sidebar/radio_group.vue'; -import { createStore } from '~/ide/stores'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; - -describe('IDE commit sidebar radio group', () => { - let wrapper; - let store; - - const createComponent = (config = {}) => { - store = createStore(); - - store.state.commit.commitAction = '2'; - store.state.commit.newBranchName = 'test-123'; - - wrapper = mount(RadioGroup, { - store, - propsData: config.props, - slots: config.slots, - directives: { - GlTooltip: createMockDirective('gl-tooltip'), - }, - }); - }; - - describe('without input', () => { - const props = { - value: '1', - label: 'test', - checked: true, - }; - - it('uses label if present', () => { - createComponent({ props }); - - expect(wrapper.text()).toContain('test'); - }); - - it('uses slot if label is not present', () => { - createComponent({ props: { value: '1', checked: true }, slots: { default: 'Testing slot' } }); - - expect(wrapper.text()).toContain('Testing slot'); - }); - - it('updates store when changing radio button', async () => { - createComponent({ props }); - - await wrapper.find('input').trigger('change'); - - expect(store.state.commit.commitAction).toBe('1'); - }); - }); - - describe('with input', () => { - const props = { - value: '2', - label: 'test', - checked: true, - showInput: true, - }; - - it('renders input box when commitAction matches value', () => { - createComponent({ props: { ...props, value: '2' } }); - - expect(wrapper.find('.form-control').exists()).toBe(true); - }); - - it('hides input when commitAction doesnt match value', () => { - createComponent({ props: { ...props, value: '1' } }); - - expect(wrapper.find('.form-control').exists()).toBe(false); - }); - - it('updates branch name in store on input', async () => { - createComponent({ props }); - - await wrapper.find('.form-control').setValue('testing-123'); - - expect(store.state.commit.newBranchName).toBe('testing-123'); - }); - - it('renders newBranchName if present', () => { - createComponent({ props }); - - const input = wrapper.find('.form-control'); - - expect(input.element.value).toBe('test-123'); - }); - }); - - describe('tooltipTitle', () => { - it('returns title when disabled', () => { - createComponent({ - props: { value: '1', label: 'test', disabled: true, title: 'test title' }, - }); - - const tooltip = getBinding(wrapper.findComponent(GlFormRadioGroup).element, 'gl-tooltip'); - expect(tooltip.value).toBe('test title'); - }); - - it('returns blank when not disabled', () => { - createComponent({ - props: { value: '1', label: 'test', title: 'test title' }, - }); - - const tooltip = getBinding(wrapper.findComponent(GlFormRadioGroup).element, 'gl-tooltip'); - - expect(tooltip.value).toBe(''); - }); - }); -}); diff --git a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js deleted file mode 100644 index d1a81dd1639..00000000000 --- a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js +++ /dev/null @@ -1,18 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import SuccessMessage from '~/ide/components/commit_sidebar/success_message.vue'; -import { createStore } from '~/ide/stores'; - -describe('IDE commit panel successful commit state', () => { - let wrapper; - - beforeEach(() => { - const store = createStore(); - store.state.committedStateSvgPath = 'committed-state'; - store.state.lastCommitMsg = 'testing commit message'; - wrapper = shallowMount(SuccessMessage, { store }); - }); - - it('renders last commit message when it exists', () => { - expect(wrapper.text()).toContain('testing commit message'); - }); -}); diff --git a/spec/frontend/ide/components/error_message_spec.js b/spec/frontend/ide/components/error_message_spec.js deleted file mode 100644 index 0ffcce8e834..00000000000 --- a/spec/frontend/ide/components/error_message_spec.js +++ /dev/null @@ -1,115 +0,0 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import ErrorMessage from '~/ide/components/error_message.vue'; - -Vue.use(Vuex); - -describe('IDE error message component', () => { - let wrapper; - - const setErrorMessageMock = jest.fn(); - const createComponent = (messageProps) => { - const fakeStore = new Vuex.Store({ - actions: { setErrorMessage: setErrorMessageMock }, - }); - - wrapper = mount(ErrorMessage, { - propsData: { - message: { - text: 'some text', - actionText: 'test action', - actionPayload: 'testActionPayload', - ...messageProps, - }, - }, - store: fakeStore, - }); - }; - - beforeEach(() => { - setErrorMessageMock.mockReset(); - }); - - const findDismissButton = () => wrapper.find('button[aria-label=Dismiss]'); - const findActionButton = () => wrapper.find('button.gl-alert-action'); - - it('renders error message', () => { - const text = 'error message'; - createComponent({ text }); - expect(wrapper.text()).toContain(text); - }); - - it('clears error message on dismiss click', () => { - createComponent(); - findDismissButton().trigger('click'); - - expect(setErrorMessageMock).toHaveBeenCalledWith(expect.any(Object), null); - }); - - describe('with action', () => { - let actionMock; - - const message = { - actionText: 'test action', - actionPayload: 'testActionPayload', - }; - - beforeEach(() => { - actionMock = jest.fn().mockResolvedValue(); - createComponent({ - ...message, - action: actionMock, - }); - }); - - it('renders action button', () => { - const button = findActionButton(); - - expect(button.exists()).toBe(true); - expect(button.text()).toContain(message.actionText); - }); - - it('does not show dismiss button', () => { - expect(findDismissButton().exists()).toBe(false); - }); - - it('dispatches action', () => { - findActionButton().trigger('click'); - - expect(actionMock).toHaveBeenCalledWith(message.actionPayload); - }); - - it('does not dispatch action when already loading', async () => { - findActionButton().trigger('click'); - actionMock.mockReset(); - findActionButton().trigger('click'); - await nextTick(); - expect(actionMock).not.toHaveBeenCalled(); - }); - - it('shows loading icon when loading', async () => { - let resolveAction; - actionMock.mockImplementation( - () => - new Promise((resolve) => { - resolveAction = resolve; - }), - ); - findActionButton().trigger('click'); - - await nextTick(); - expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(true); - resolveAction(); - }); - - it('hides loading icon when operation finishes', async () => { - findActionButton().trigger('click'); - await actionMock(); - await nextTick(); - expect(wrapper.findComponent(GlLoadingIcon).isVisible()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js deleted file mode 100644 index bd5bcccf597..00000000000 --- a/spec/frontend/ide/components/file_row_extra_spec.js +++ /dev/null @@ -1,145 +0,0 @@ -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { mount } from '@vue/test-utils'; -import FileRowExtra from '~/ide/components/file_row_extra.vue'; -import { createStoreOptions } from '~/ide/stores'; -import { file } from '../helpers'; - -describe('IDE extra file row component', () => { - let wrapper; - let store; - let unstagedFilesCount = 0; - let stagedFilesCount = 0; - let changesCount = 0; - - const createComponent = (fileProps) => { - const storeConfig = createStoreOptions(); - - store = new Vuex.Store({ - ...storeConfig, - getters: { - getUnstagedFilesCountForPath: () => () => unstagedFilesCount, - getStagedFilesCountForPath: () => () => stagedFilesCount, - getChangesInFolder: () => () => changesCount, - }, - }); - - wrapper = mount(FileRowExtra, { - store, - propsData: { - file: { - ...file('test'), - type: 'tree', - ...fileProps, - }, - dropdownOpen: false, - }, - }); - }; - - afterEach(() => { - stagedFilesCount = 0; - unstagedFilesCount = 0; - changesCount = 0; - }); - - describe('folder changes tooltip', () => { - [ - { input: 1, output: '1 changed file' }, - { input: 2, output: '2 changed files' }, - ].forEach(({ input, output }) => { - it('shows changed files count if changes count is not 0', () => { - changesCount = input; - createComponent(); - - expect(wrapper.find('.ide-file-modified').attributes('title')).toBe(output); - }); - }); - }); - - describe('show tree changes count', () => { - const findTreeChangesCount = () => wrapper.find('.ide-tree-changes'); - - it('does not show for blobs', () => { - createComponent({ type: 'blob' }); - - expect(findTreeChangesCount().exists()).toBe(false); - }); - - it('does not show when changes count is 0', () => { - createComponent({ type: 'tree' }); - - expect(findTreeChangesCount().exists()).toBe(false); - }); - - it('does not show when tree is open', () => { - changesCount = 1; - createComponent({ type: 'tree', opened: true }); - - expect(findTreeChangesCount().exists()).toBe(false); - }); - - it('shows for trees with changes', () => { - changesCount = 1; - createComponent({ type: 'tree', opened: false }); - - expect(findTreeChangesCount().exists()).toBe(true); - }); - }); - - describe('changes file icon', () => { - const findChangedFileIcon = () => wrapper.find('.file-changed-icon'); - - it('hides when file is not changed', () => { - createComponent(); - - expect(findChangedFileIcon().exists()).toBe(false); - }); - - it('shows when file is changed', () => { - createComponent({ type: 'blob', changed: true }); - - expect(findChangedFileIcon().exists()).toBe(true); - }); - - it('shows when file is staged', () => { - createComponent({ type: 'blob', staged: true }); - - expect(findChangedFileIcon().exists()).toBe(true); - }); - - it('shows when file is a tempFile', () => { - createComponent({ type: 'blob', tempFile: true }); - - expect(findChangedFileIcon().exists()).toBe(true); - }); - - it('shows when file is renamed', () => { - createComponent({ type: 'blob', prevPath: 'original-file' }); - - expect(findChangedFileIcon().exists()).toBe(true); - }); - - it('hides when tree is renamed', () => { - createComponent({ type: 'tree', prevPath: 'original-path' }); - - expect(findChangedFileIcon().exists()).toBe(false); - }); - }); - - describe('merge request icon', () => { - const findMergeRequestIcon = () => wrapper.find('[data-testid="merge-request-icon"]'); - - it('hides when not a merge request change', () => { - createComponent(); - - expect(findMergeRequestIcon().exists()).toBe(false); - }); - - it('shows when a merge request change', () => { - createComponent({ mrChange: true }); - - expect(findMergeRequestIcon().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js deleted file mode 100644 index b8c850fdd13..00000000000 --- a/spec/frontend/ide/components/file_templates/bar_spec.js +++ /dev/null @@ -1,90 +0,0 @@ -import { nextTick } from 'vue'; -import { mount } from '@vue/test-utils'; -import Bar from '~/ide/components/file_templates/bar.vue'; -import { createStore } from '~/ide/stores'; -import { file } from '../../helpers'; - -describe('IDE file templates bar component', () => { - let wrapper; - let store; - - beforeEach(() => { - store = createStore(); - jest.spyOn(store, 'dispatch').mockImplementation(); - - store.state.openFiles.push({ - ...file('file'), - opened: true, - active: true, - }); - - wrapper = mount(Bar, { store }); - }); - - describe('template type dropdown', () => { - it('renders dropdown component', () => { - expect(wrapper.find('.dropdown').text()).toContain('Choose a type'); - }); - - it('calls setSelectedTemplateType when clicking item', async () => { - await wrapper.find('.dropdown-menu button').trigger('click'); - - expect(store.dispatch).toHaveBeenCalledWith('fileTemplates/setSelectedTemplateType', { - name: '.gitlab-ci.yml', - key: 'gitlab_ci_ymls', - }); - }); - }); - - describe('template dropdown', () => { - beforeEach(() => { - store.state.fileTemplates.templates = [ - { - name: 'test', - }, - ]; - store.state.fileTemplates.selectedTemplateType = { - name: '.gitlab-ci.yml', - key: 'gitlab_ci_ymls', - }; - }); - - it('renders dropdown component', () => { - expect(wrapper.findAll('.dropdown').at(1).text()).toContain('Choose a template'); - }); - - it('calls fetchTemplate on dropdown open', async () => { - await wrapper.findAll('.dropdown-menu').at(1).find('button').trigger('click'); - - expect(store.dispatch).toHaveBeenCalledWith('fileTemplates/fetchTemplate', { - name: 'test', - }); - }); - }); - - const findUndoButton = () => wrapper.find('.btn-default-secondary'); - it('shows undo button if updateSuccess is true', async () => { - store.state.fileTemplates.updateSuccess = true; - await nextTick(); - - expect(findUndoButton().isVisible()).toBe(true); - }); - - it('calls undoFileTemplate when clicking undo button', async () => { - await findUndoButton().trigger('click'); - - expect(store.dispatch).toHaveBeenCalledWith('fileTemplates/undoFileTemplate', undefined); - }); - - it('calls setSelectedTemplateType if activeFile name matches a template', async () => { - const fileName = '.gitlab-ci.yml'; - store.state.openFiles = [{ ...file(fileName), opened: true, active: true }]; - - await nextTick(); - - expect(store.dispatch).toHaveBeenCalledWith('fileTemplates/setSelectedTemplateType', { - name: fileName, - key: 'gitlab_ci_ymls', - }); - }); -}); diff --git a/spec/frontend/ide/components/ide_file_row_spec.js b/spec/frontend/ide/components/ide_file_row_spec.js deleted file mode 100644 index c9d4c23b475..00000000000 --- a/spec/frontend/ide/components/ide_file_row_spec.js +++ /dev/null @@ -1,109 +0,0 @@ -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import FileRowExtra from '~/ide/components/file_row_extra.vue'; -import IdeFileRow from '~/ide/components/ide_file_row.vue'; -import { createStore } from '~/ide/stores'; -import FileRow from '~/vue_shared/components/file_row.vue'; - -Vue.use(Vuex); - -const TEST_EXTRA_PROPS = { - testattribute: 'abc', -}; - -const defaultComponentProps = (type = 'tree') => ({ - level: 4, - file: { - type, - name: 'js', - }, -}); - -describe('Ide File Row component', () => { - let wrapper; - - const createComponent = (props = {}, options = {}) => { - wrapper = mount(IdeFileRow, { - propsData: { - ...defaultComponentProps(), - ...props, - }, - store: createStore(), - ...options, - }); - }; - - const findFileRowExtra = () => wrapper.findComponent(FileRowExtra); - const findFileRow = () => wrapper.findComponent(FileRow); - const hasDropdownOpen = () => findFileRowExtra().props('dropdownOpen'); - - it('fileRow component has listeners', async () => { - const toggleTreeOpen = jest.fn(); - createComponent( - {}, - { - listeners: { - toggleTreeOpen, - }, - }, - ); - - findFileRow().vm.$emit('toggleTreeOpen'); - - await nextTick(); - expect(toggleTreeOpen).toHaveBeenCalled(); - }); - - describe('default', () => { - beforeEach(() => { - createComponent(TEST_EXTRA_PROPS); - }); - - it('renders file row component', () => { - const fileRow = findFileRow(); - - expect(fileRow.props()).toEqual(expect.objectContaining(defaultComponentProps())); - expect(fileRow.attributes()).toEqual(expect.objectContaining(TEST_EXTRA_PROPS)); - }); - - it('renders file row extra', () => { - const extra = findFileRowExtra(); - - expect(extra.exists()).toBe(true); - expect(extra.props()).toEqual({ - file: defaultComponentProps().file, - dropdownOpen: false, - }); - }); - }); - - describe('with open dropdown', () => { - beforeEach(async () => { - createComponent(); - - findFileRowExtra().vm.$emit('toggle', true); - - await nextTick(); - }); - - it('shows open dropdown', () => { - expect(hasDropdownOpen()).toBe(true); - }); - - it('hides dropdown when mouseleave', async () => { - findFileRow().vm.$emit('mouseleave'); - - await nextTick(); - expect(hasDropdownOpen()).toEqual(false); - }); - - it('hides dropdown on toggle', async () => { - findFileRowExtra().vm.$emit('toggle', false); - - await nextTick(); - expect(hasDropdownOpen()).toEqual(false); - }); - }); -}); diff --git a/spec/frontend/ide/components/ide_project_header_spec.js b/spec/frontend/ide/components/ide_project_header_spec.js deleted file mode 100644 index 7613f407e45..00000000000 --- a/spec/frontend/ide/components/ide_project_header_spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import IDEProjectHeader from '~/ide/components/ide_project_header.vue'; -import ProjectAvatar from '~/vue_shared/components/project_avatar.vue'; - -const mockProject = { - id: 1, - name: 'test proj', - avatar_url: 'https://gitlab.com', - path_with_namespace: 'path/with-namespace', - web_url: 'https://gitlab.com/project', -}; - -describe('IDE project header', () => { - let wrapper; - - const findProjectAvatar = () => wrapper.findComponent(ProjectAvatar); - const findProjectLink = () => wrapper.find('[data-testid="go-to-project-link"'); - - const createComponent = () => { - wrapper = shallowMount(IDEProjectHeader, { propsData: { project: mockProject } }); - }; - - describe('template', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders ProjectAvatar with correct props', () => { - expect(findProjectAvatar().props()).toMatchObject({ - projectId: mockProject.id, - projectName: mockProject.name, - projectAvatarUrl: mockProject.avatar_url, - }); - }); - - it('renders a link to the project URL', () => { - const link = findProjectLink(); - expect(link.exists()).toBe(true); - expect(link.attributes('href')).toBe(mockProject.web_url); - }); - }); -}); diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js deleted file mode 100644 index b052d7f5cc6..00000000000 --- a/spec/frontend/ide/components/ide_review_spec.js +++ /dev/null @@ -1,109 +0,0 @@ -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { keepAlive } from 'helpers/keep_alive_component_helper'; -import { trimText } from 'helpers/text_helper'; -import EditorModeDropdown from '~/ide/components/editor_mode_dropdown.vue'; -import IdeReview from '~/ide/components/ide_review.vue'; -import { createStore } from '~/ide/stores'; -import { file } from '../helpers'; -import { projectData } from '../mock_data'; - -Vue.use(Vuex); - -describe('IDE review mode', () => { - let wrapper; - let store; - let dispatch; - - beforeEach(() => { - store = createStore(); - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'main'; - store.state.projects.abcproject = { ...projectData }; - store.state.trees = { - ...store.state.trees, - 'abcproject/main': { - tree: [file('fileName')], - loading: false, - }, - }; - - dispatch = jest.spyOn(store, 'dispatch'); - - wrapper = mount(keepAlive(IdeReview), { - store, - }); - }); - - const findEditorModeDropdown = () => wrapper.findComponent(EditorModeDropdown); - - it('renders list of files', () => { - expect(wrapper.text()).toContain('fileName'); - }); - - describe('activated', () => { - beforeEach(async () => { - store.state.viewer = 'editor'; - - await wrapper.vm.reactivate(); - }); - - it('re initializes the component', () => { - expect(dispatch).toHaveBeenCalledWith('updateViewer', 'diff'); - }); - - it('updates viewer to "diff" by default', () => { - expect(store.state.viewer).toBe('diff'); - }); - - describe('merge request is defined', () => { - beforeEach(async () => { - store.state.currentMergeRequestId = '1'; - store.state.projects.abcproject.mergeRequests['1'] = { - iid: 123, - web_url: 'testing123', - }; - - await wrapper.vm.reactivate(); - }); - - it('updates viewer to "mrdiff"', () => { - expect(store.state.viewer).toBe('mrdiff'); - }); - }); - }); - - describe('merge request', () => { - beforeEach(async () => { - store.state.currentMergeRequestId = '1'; - store.state.projects.abcproject.mergeRequests['1'] = { - iid: 123, - web_url: 'testing123', - }; - - await nextTick(); - }); - - it('renders edit dropdown', () => { - expect(findEditorModeDropdown().exists()).toBe(true); - }); - - it('renders merge request link & IID', async () => { - store.state.viewer = 'mrdiff'; - - await nextTick(); - - expect(trimText(wrapper.text())).toContain('Merge request (!123)'); - }); - - it('changes text to latest changes when viewer is not mrdiff', async () => { - store.state.viewer = 'diff'; - - await nextTick(); - - expect(wrapper.text()).toContain('Latest changes'); - }); - }); -}); diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js deleted file mode 100644 index fe92bd2c51e..00000000000 --- a/spec/frontend/ide/components/ide_side_bar_spec.js +++ /dev/null @@ -1,111 +0,0 @@ -import { GlSkeletonLoader } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import waitForPromises from 'helpers/wait_for_promises'; -import IdeReview from '~/ide/components/ide_review.vue'; -import IdeSidebar from '~/ide/components/ide_side_bar.vue'; -import IdeTree from '~/ide/components/ide_tree.vue'; -import RepoCommitSection from '~/ide/components/repo_commit_section.vue'; -import { leftSidebarViews } from '~/ide/constants'; -import { createStore } from '~/ide/stores'; -import { projectData } from '../mock_data'; - -Vue.use(Vuex); - -describe('IdeSidebar', () => { - let wrapper; - let store; - - function createComponent({ view = leftSidebarViews.edit.name } = {}) { - store = createStore(); - - store.state.currentProjectId = 'abcproject'; - store.state.projects.abcproject = projectData; - store.state.currentActivityView = view; - - return mount(IdeSidebar, { - store, - }); - } - - it('renders a sidebar', () => { - wrapper = createComponent(); - - expect(wrapper.find('[data-testid="ide-side-bar-inner"]').exists()).toBe(true); - }); - - it('renders loading components', async () => { - wrapper = createComponent(); - - store.state.loading = true; - - await nextTick(); - - expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(3); - }); - - describe('deferred rendering components', () => { - it('fetches components on demand', async () => { - wrapper = createComponent(); - - expect(wrapper.findComponent(IdeTree).exists()).toBe(true); - expect(wrapper.findComponent(IdeReview).exists()).toBe(false); - expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(false); - - store.state.currentActivityView = leftSidebarViews.review.name; - await waitForPromises(); - await nextTick(); - - expect(wrapper.findComponent(IdeTree).exists()).toBe(false); - expect(wrapper.findComponent(IdeReview).exists()).toBe(true); - expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(false); - - store.state.currentActivityView = leftSidebarViews.commit.name; - await waitForPromises(); - await nextTick(); - - expect(wrapper.findComponent(IdeTree).exists()).toBe(false); - expect(wrapper.findComponent(IdeReview).exists()).toBe(false); - expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(true); - }); - it.each` - view | tree | review | commit - ${leftSidebarViews.edit.name} | ${true} | ${false} | ${false} - ${leftSidebarViews.review.name} | ${false} | ${true} | ${false} - ${leftSidebarViews.commit.name} | ${false} | ${false} | ${true} - `('renders correct panels for $view', async ({ view, tree, review, commit } = {}) => { - wrapper = createComponent({ - view, - }); - await waitForPromises(); - await nextTick(); - - expect(wrapper.findComponent(IdeTree).exists()).toBe(tree); - expect(wrapper.findComponent(IdeReview).exists()).toBe(review); - expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(commit); - }); - }); - - it('keeps the current activity view components alive', async () => { - wrapper = createComponent(); - - const ideTreeComponent = wrapper.findComponent(IdeTree).element; - - store.state.currentActivityView = leftSidebarViews.commit.name; - await waitForPromises(); - await nextTick(); - - expect(wrapper.findComponent(IdeTree).exists()).toBe(false); - expect(wrapper.findComponent(RepoCommitSection).exists()).toBe(true); - - store.state.currentActivityView = leftSidebarViews.edit.name; - - await waitForPromises(); - await nextTick(); - - // reference to the elements remains the same, meaning the components were kept alive - expect(wrapper.findComponent(IdeTree).element).toEqual(ideTreeComponent); - }); -}); diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js deleted file mode 100644 index 0137e4e131b..00000000000 --- a/spec/frontend/ide/components/ide_sidebar_nav_spec.js +++ /dev/null @@ -1,105 +0,0 @@ -import { GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue'; -import { SIDE_RIGHT, SIDE_LEFT } from '~/ide/constants'; -import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; - -const TEST_TABS = [ - { - title: 'Lorem', - icon: 'chevron-lg-up', - views: [{ name: 'lorem-1' }, { name: 'lorem-2' }], - }, - { - title: 'Ipsum', - icon: 'chevron-lg-down', - views: [{ name: 'ipsum-1' }, { name: 'ipsum-2' }], - }, -]; -const TEST_CURRENT_INDEX = 1; -const TEST_CURRENT_VIEW = TEST_TABS[TEST_CURRENT_INDEX].views[1].name; -const TEST_OPEN_VIEW = TEST_TABS[TEST_CURRENT_INDEX].views[0]; - -describe('ide/components/ide_sidebar_nav', () => { - let wrapper; - - const createComponent = (props = {}) => { - wrapper = shallowMount(IdeSidebarNav, { - propsData: { - tabs: TEST_TABS, - currentView: TEST_CURRENT_VIEW, - isOpen: false, - ...props, - }, - directives: { - GlTooltip: createMockDirective('gl-tooltip'), - }, - }); - }; - - const findButtons = () => wrapper.findAll('li button'); - const findButtonsData = () => - findButtons().wrappers.map((button) => { - return { - title: button.attributes('title'), - ariaLabel: button.attributes('aria-label'), - classes: button.classes(), - icon: button.findComponent(GlIcon).props('name'), - tooltip: getBinding(button.element, 'gl-tooltip').value, - }; - }); - const clickTab = () => findButtons().at(TEST_CURRENT_INDEX).trigger('click'); - - describe.each` - isOpen | side | otherSide | classes | classesObj | emitEvent | emitArg - ${false} | ${SIDE_LEFT} | ${SIDE_RIGHT} | ${[]} | ${{}} | ${'open'} | ${[TEST_OPEN_VIEW]} - ${false} | ${SIDE_RIGHT} | ${SIDE_LEFT} | ${['is-right']} | ${{}} | ${'open'} | ${[TEST_OPEN_VIEW]} - ${true} | ${SIDE_RIGHT} | ${SIDE_LEFT} | ${['is-right']} | ${{ [TEST_CURRENT_INDEX]: ['active'] }} | ${'close'} | ${[]} - `( - 'with side = $side, isOpen = $isOpen', - ({ isOpen, side, otherSide, classes, classesObj, emitEvent, emitArg }) => { - let bsTooltipHide; - - beforeEach(() => { - createComponent({ isOpen, side }); - - bsTooltipHide = jest.fn(); - wrapper.vm.$root.$on(BV_HIDE_TOOLTIP, bsTooltipHide); - }); - - it('renders buttons', () => { - expect(findButtonsData()).toEqual( - TEST_TABS.map((tab, index) => ({ - title: tab.title, - ariaLabel: tab.title, - classes: ['ide-sidebar-link', ...classes, ...(classesObj[index] || [])], - icon: tab.icon, - tooltip: { - container: 'body', - placement: otherSide, - }, - })), - ); - }); - - it('when tab clicked, emits event', () => { - expect(wrapper.emitted()).toEqual({}); - - clickTab(); - - expect(wrapper.emitted()).toEqual({ - [emitEvent]: [emitArg], - }); - }); - - it('when tab clicked, hides tooltip', () => { - expect(bsTooltipHide).not.toHaveBeenCalled(); - - clickTab(); - - expect(bsTooltipHide).toHaveBeenCalled(); - }); - }, - ); -}); diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js deleted file mode 100644 index f17fc60a2fe..00000000000 --- a/spec/frontend/ide/components/ide_spec.js +++ /dev/null @@ -1,249 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlBroadcastMessage, GlLink, GlSprintf } from '@gitlab/ui'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import waitForPromises from 'helpers/wait_for_promises'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import CannotPushCodeAlert from '~/ide/components/cannot_push_code_alert.vue'; -import ErrorMessage from '~/ide/components/error_message.vue'; -import Ide from '~/ide/components/ide.vue'; -import eventHub from '~/ide/eventhub'; -import { MSG_CANNOT_PUSH_CODE_GO_TO_FORK, MSG_GO_TO_FORK } from '~/ide/messages'; -import { createStore } from '~/ide/stores'; -import { file } from '../helpers'; -import { projectData } from '../mock_data'; - -Vue.use(Vuex); - -const TEST_FORK_IDE_PATH = '/test/ide/path'; -const MSG_ARE_YOU_SURE = 'Are you sure you want to lose unsaved changes?'; - -describe('WebIDE', () => { - const emptyProjData = { ...projectData, empty_repo: true, branches: {} }; - - let store; - let wrapper; - - const createComponent = ({ projData = emptyProjData, state = {} } = {}) => { - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'main'; - store.state.projects.abcproject = projData && { ...projData }; - store.state.trees['abcproject/main'] = { - tree: [], - loading: false, - }; - Object.keys(state).forEach((key) => { - store.state[key] = state[key]; - }); - - wrapper = shallowMount(Ide, { - store, - stubs: { - GlSprintf, - }, - }); - }; - - const findAlert = () => wrapper.findComponent(CannotPushCodeAlert); - - const findBroadcastMessage = () => wrapper.findComponent(GlBroadcastMessage); - const callOnBeforeUnload = (e = {}) => window.onbeforeunload(e); - - beforeAll(() => { - // HACK: Workaround readonly property in Jest - Object.defineProperty(window, 'onbeforeunload', { - writable: true, - }); - }); - - beforeEach(() => { - stubPerformanceWebAPI(); - - store = createStore(); - }); - - afterEach(() => { - window.onbeforeunload = null; - }); - - describe('removal announcement', () => { - beforeEach(() => { - createComponent(); - }); - - it('displays removal announcement', () => { - expect(findBroadcastMessage().text()).toMatch( - /The legacy Vue-based GitLab Web IDE will be removed in GitLab 18.0/, - ); - expect(findBroadcastMessage().text()).toMatch( - /To prepare for this removal, see deprecations and removals./, - ); - }); - - it('displays a banner with a link to the deprecation announcement', () => { - const glLink = findBroadcastMessage().findComponent(GlLink); - expect(glLink.attributes('href')).toBe( - '/help/update/deprecations.md#legacy-web-ide-is-deprecated', - ); - }); - - it('does not allow dismissing the announcement', () => { - expect(findBroadcastMessage().props()).toMatchObject({ - dismissible: false, - iconName: 'warning', - theme: 'red', - }); - }); - }); - - describe('ide component, empty repo', () => { - beforeEach(() => { - createComponent({ - projData: { - empty_repo: true, - }, - }); - }); - - it('renders "New file" button in empty repo', () => { - expect(wrapper.find('[title="New file"]').exists()).toBe(true); - }); - }); - - describe('ide component, non-empty repo', () => { - describe('error message', () => { - it.each` - errorMessage | exists - ${null} | ${false} - ${{ text: 'error' }} | ${true} - `( - 'should error message exists=$exists when errorMessage=$errorMessage', - async ({ errorMessage, exists }) => { - createComponent({ - state: { - errorMessage, - }, - }); - - await waitForPromises(); - - expect(wrapper.findComponent(ErrorMessage).exists()).toBe(exists); - }, - ); - }); - - describe('onBeforeUnload', () => { - it('returns undefined when no staged files or changed files', () => { - createComponent(); - - expect(callOnBeforeUnload()).toBe(undefined); - }); - - it('returns warning text when their are changed files', () => { - createComponent({ - state: { - changedFiles: [file()], - }, - }); - - const e = {}; - - expect(callOnBeforeUnload(e)).toBe(MSG_ARE_YOU_SURE); - expect(e.returnValue).toBe(MSG_ARE_YOU_SURE); - }); - - it('returns warning text when their are staged files', () => { - createComponent({ - state: { - stagedFiles: [file()], - }, - }); - - const e = {}; - - expect(callOnBeforeUnload(e)).toBe(MSG_ARE_YOU_SURE); - expect(e.returnValue).toBe(MSG_ARE_YOU_SURE); - }); - - it('returns undefined once after "skip-beforeunload" was emitted', () => { - createComponent({ - state: { - stagedFiles: [file()], - }, - }); - - eventHub.$emit('skip-beforeunload'); - const e = {}; - - expect(callOnBeforeUnload()).toBe(undefined); - expect(e.returnValue).toBe(undefined); - - expect(callOnBeforeUnload(e)).toBe(MSG_ARE_YOU_SURE); - expect(e.returnValue).toBe(MSG_ARE_YOU_SURE); - }); - }); - - describe('non-existent branch', () => { - it('does not render "New file" button for non-existent branch when repo is not empty', () => { - createComponent({ - state: { - projects: {}, - }, - }); - - expect(wrapper.find('[title="New file"]').exists()).toBe(false); - }); - }); - - describe('branch with files', () => { - beforeEach(() => { - createComponent({ - projData: { - empty_repo: false, - }, - }); - }); - - it('does not render "New file" button', () => { - expect(wrapper.find('[title="New file"]').exists()).toBe(false); - }); - }); - }); - - it('when user cannot push code, shows an alert', () => { - store.state.links = { - forkInfo: { - ide_path: TEST_FORK_IDE_PATH, - }, - }; - - createComponent({ - projData: { - userPermissions: { - pushCode: false, - }, - }, - }); - - expect(findAlert().props()).toMatchObject({ - message: MSG_CANNOT_PUSH_CODE_GO_TO_FORK, - action: { - href: TEST_FORK_IDE_PATH, - text: MSG_GO_TO_FORK, - }, - }); - }); - - it.each` - desc | projData - ${'when user can push code'} | ${{ userPermissions: { pushCode: true } }} - ${'when project is not ready'} | ${null} - `('$desc, no alert is shown', ({ projData }) => { - createComponent({ - projData, - }); - - expect(findAlert().exists()).toBe(false); - }); -}); diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js deleted file mode 100644 index eb51faaaa16..00000000000 --- a/spec/frontend/ide/components/ide_status_bar_spec.js +++ /dev/null @@ -1,123 +0,0 @@ -import { clone } from 'lodash'; -import { TEST_HOST } from 'helpers/test_constants'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import IdeStatusBar from '~/ide/components/ide_status_bar.vue'; -import IdeStatusMR from '~/ide/components/ide_status_mr.vue'; -import { rightSidebarViews } from '~/ide/constants'; -import { createStore } from '~/ide/stores'; -import { projectData } from '../mock_data'; - -const TEST_PROJECT_ID = 'abcproject'; -const TEST_MERGE_REQUEST_ID = '9001'; -const TEST_MERGE_REQUEST_URL = `${TEST_HOST}merge-requests/${TEST_MERGE_REQUEST_ID}`; - -jest.mock('~/lib/utils/poll'); - -describe('IdeStatusBar component', () => { - let wrapper; - const dummyIntervalId = 1337; - let dispatchMock; - - const findMRStatus = () => wrapper.findComponent(IdeStatusMR); - - const mountComponent = (state = {}) => { - const store = createStore(); - store.replaceState({ - ...store.state, - currentBranchId: 'main', - currentProjectId: TEST_PROJECT_ID, - projects: { - ...store.state.projects, - [TEST_PROJECT_ID]: clone(projectData), - }, - ...state, - }); - - wrapper = mountExtended(IdeStatusBar, { store }); - dispatchMock = jest.spyOn(store, 'dispatch'); - }; - - beforeEach(() => { - jest.spyOn(window, 'setInterval').mockReturnValue(dummyIntervalId); - }); - - const findCommitShaLink = () => wrapper.findByTestId('commit-sha-content'); - - describe('default', () => { - it('triggers a setInterval', () => { - mountComponent(); - - expect(window.setInterval).toHaveBeenCalledTimes(1); - }); - - it('renders the statusbar', () => { - mountComponent(); - - expect(wrapper.classes()).toEqual(['ide-status-bar']); - }); - - describe('getCommitPath', () => { - it('returns the path to the commit details', () => { - mountComponent(); - expect(findCommitShaLink().attributes('href')).toBe('/commit/abc123de'); - }); - }); - - describe('pipeline status', () => { - it('opens right sidebar on clicking icon', () => { - const pipelines = { - latestPipeline: { - details: { - status: { - text: 'success', - details_path: 'test', - icon: 'status_success', - }, - }, - commit: { - author_gravatar_url: 'www', - }, - }, - }; - mountComponent({ pipelines }); - - wrapper.find('button').trigger('click'); - - expect(dispatchMock).toHaveBeenCalledWith('rightPane/open', rightSidebarViews.pipelines); - }); - }); - - it('does not show merge request status', () => { - mountComponent(); - - expect(findMRStatus().exists()).toBe(false); - }); - }); - - describe('with merge request in store', () => { - beforeEach(() => { - const state = { - currentMergeRequestId: TEST_MERGE_REQUEST_ID, - projects: { - [TEST_PROJECT_ID]: { - ...clone(projectData), - mergeRequests: { - [TEST_MERGE_REQUEST_ID]: { - web_url: TEST_MERGE_REQUEST_URL, - references: { - short: `!${TEST_MERGE_REQUEST_ID}`, - }, - }, - }, - }, - }, - }; - mountComponent(state); - }); - - it('shows merge request status', () => { - expect(findMRStatus().text()).toBe(`Merge request !${TEST_MERGE_REQUEST_ID}`); - expect(findMRStatus().find('a').attributes('href')).toBe(TEST_MERGE_REQUEST_URL); - }); - }); -}); diff --git a/spec/frontend/ide/components/ide_status_list_spec.js b/spec/frontend/ide/components/ide_status_list_spec.js deleted file mode 100644 index e353a4de054..00000000000 --- a/spec/frontend/ide/components/ide_status_list_spec.js +++ /dev/null @@ -1,101 +0,0 @@ -import { GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import IdeStatusList from '~/ide/components/ide_status_list.vue'; -import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync_status_safe.vue'; - -const TEST_FILE = { - name: 'lorem.md', - content: 'abc\nndef', - permalink: '/lorem.md', -}; -const TEST_FILE_EDITOR = { - fileLanguage: 'markdown', - editorRow: 3, - editorColumn: 23, -}; -const TEST_EDITOR_POSITION = `${TEST_FILE_EDITOR.editorRow}:${TEST_FILE_EDITOR.editorColumn}`; - -Vue.use(Vuex); - -describe('ide/components/ide_status_list', () => { - let activeFileEditor; - let activeFile; - let store; - let wrapper; - - const findLink = () => wrapper.findComponent(GlLink); - const createComponent = (options = {}) => { - store = new Vuex.Store({ - getters: { - activeFile: () => activeFile, - }, - modules: { - editor: { - namespaced: true, - getters: { - activeFileEditor: () => activeFileEditor, - }, - }, - }, - }); - - wrapper = shallowMount(IdeStatusList, { - store, - ...options, - }); - }; - - beforeEach(() => { - activeFile = TEST_FILE; - activeFileEditor = TEST_FILE_EDITOR; - }); - - afterEach(() => { - store = null; - }); - - describe('with regular file', () => { - beforeEach(() => { - createComponent(); - }); - - it('shows a link to the file that contains the file name', () => { - expect(findLink().attributes('href')).toBe(TEST_FILE.permalink); - expect(findLink().text()).toBe(TEST_FILE.name); - }); - - it('shows file eol', () => { - expect(wrapper.text()).not.toContain('CRLF'); - expect(wrapper.text()).toContain('LF'); - }); - - it('shows file editor position', () => { - expect(wrapper.text()).toContain(TEST_EDITOR_POSITION); - }); - - it('shows file language', () => { - expect(wrapper.text()).toContain(TEST_FILE_EDITOR.fileLanguage); - }); - }); - - describe('with binary file', () => { - beforeEach(() => { - activeFile.name = 'abc.dat'; - activeFile.content = '🐱'; // non-ascii binary content - createComponent(); - }); - - it('does not show file editor position', () => { - expect(wrapper.text()).not.toContain(TEST_EDITOR_POSITION); - }); - }); - - it('renders terminal sync status', () => { - createComponent(); - - expect(wrapper.findComponent(TerminalSyncStatusSafe).exists()).toBe(true); - }); -}); diff --git a/spec/frontend/ide/components/ide_status_mr_spec.js b/spec/frontend/ide/components/ide_status_mr_spec.js deleted file mode 100644 index 3501ecce061..00000000000 --- a/spec/frontend/ide/components/ide_status_mr_spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import { GlIcon, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { TEST_HOST } from 'helpers/test_constants'; -import IdeStatusMr from '~/ide/components/ide_status_mr.vue'; - -const TEST_TEXT = '!9001'; -const TEST_URL = `${TEST_HOST}merge-requests/9001`; - -describe('ide/components/ide_status_mr', () => { - let wrapper; - - const createComponent = (props) => { - wrapper = shallowMount(IdeStatusMr, { - propsData: props, - }); - }; - const findIcon = () => wrapper.findComponent(GlIcon); - const findLink = () => wrapper.findComponent(GlLink); - - describe('when mounted', () => { - beforeEach(() => { - createComponent({ - text: TEST_TEXT, - url: TEST_URL, - }); - }); - - it('renders icon', () => { - const icon = findIcon(); - - expect(icon.exists()).toBe(true); - expect(icon.props()).toEqual( - expect.objectContaining({ - name: 'merge-request', - }), - ); - }); - - it('renders link', () => { - const link = findLink(); - - expect(link.exists()).toBe(true); - expect(link.attributes()).toEqual( - expect.objectContaining({ - href: TEST_URL, - }), - ); - expect(link.text()).toEqual(TEST_TEXT); - }); - - it('renders text', () => { - expect(wrapper.text()).toBe(`Merge request ${TEST_TEXT}`); - }); - }); -}); diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js deleted file mode 100644 index 2658f99f9d4..00000000000 --- a/spec/frontend/ide/components/ide_tree_list_spec.js +++ /dev/null @@ -1,70 +0,0 @@ -import { GlSkeletonLoader } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import IdeTreeList from '~/ide/components/ide_tree_list.vue'; -import { createStore } from '~/ide/stores'; -import FileTree from '~/vue_shared/components/file_tree.vue'; -import { file } from '../helpers'; -import { projectData } from '../mock_data'; - -describe('IdeTreeList component', () => { - let wrapper; - - const mountComponent = ({ tree, loading = false } = {}) => { - const store = createStore(); - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'main'; - store.state.projects.abcproject = { ...projectData }; - store.state.trees = { - ...store.state.trees, - 'abcproject/main': { tree, loading }, - }; - - wrapper = shallowMount(IdeTreeList, { - propsData: { - viewerType: 'edit', - }, - store, - }); - }; - - describe('normal branch', () => { - const tree = [file('fileName')]; - - it('emits tree-ready event', () => { - mountComponent({ tree }); - - expect(wrapper.emitted('tree-ready')).toEqual([[]]); - }); - - it('renders loading indicator', () => { - mountComponent({ tree, loading: true }); - - expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(3); - }); - - it('renders list of files', () => { - mountComponent({ tree }); - - expect(wrapper.findAllComponents(FileTree)).toHaveLength(1); - expect(wrapper.findComponent(FileTree).props('file')).toEqual(tree[0]); - }); - }); - - describe('empty-branch state', () => { - beforeEach(() => { - mountComponent({ tree: [] }); - }); - - it('emits tree-ready event', () => { - expect(wrapper.emitted('tree-ready')).toEqual([[]]); - }); - - it('does not render files', () => { - expect(wrapper.findAllComponents(FileTree)).toHaveLength(0); - }); - - it('renders empty state text', () => { - expect(wrapper.text()).toBe('No files'); - }); - }); -}); diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js deleted file mode 100644 index 1d6158d1e96..00000000000 --- a/spec/frontend/ide/components/ide_tree_spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import { mount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { viewerTypes } from '~/ide/constants'; -import IdeTree from '~/ide/components/ide_tree.vue'; -import { createStoreOptions } from '~/ide/stores'; -import { file } from '../helpers'; -import { projectData } from '../mock_data'; - -Vue.use(Vuex); - -describe('IdeTree', () => { - let store; - let wrapper; - - const actionSpies = { - updateViewer: jest.fn(), - }; - - const testState = { - currentProjectId: 'abcproject', - currentBranchId: 'main', - projects: { - abcproject: { ...projectData }, - }, - trees: { - 'abcproject/main': { - tree: [file('fileName')], - loading: false, - }, - }, - }; - - const createComponent = (replaceState) => { - const defaultStore = createStoreOptions(); - - store = new Vuex.Store({ - ...defaultStore, - state: { - ...defaultStore.state, - ...testState, - replaceState, - }, - actions: { - ...defaultStore.actions, - ...actionSpies, - }, - }); - - wrapper = mount(IdeTree, { - store, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - actionSpies.updateViewer.mockClear(); - }); - - describe('renders properly', () => { - it('renders list of files', () => { - expect(wrapper.text()).toContain('fileName'); - }); - }); - - describe('activated', () => { - beforeEach(() => { - createComponent({ - viewer: viewerTypes.diff, - }); - }); - - it('re initializes the component', () => { - expect(actionSpies.updateViewer).toHaveBeenCalled(); - }); - - it('updates viewer to "editor" by default', () => { - expect(actionSpies.updateViewer).toHaveBeenCalledWith(expect.any(Object), viewerTypes.edit); - }); - }); -}); diff --git a/spec/frontend/ide/components/jobs/detail/description_spec.js b/spec/frontend/ide/components/jobs/detail/description_spec.js deleted file mode 100644 index 909bd1f7c90..00000000000 --- a/spec/frontend/ide/components/jobs/detail/description_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { GlIcon } from '@gitlab/ui'; -import Description from '~/ide/components/jobs/detail/description.vue'; -import { jobs } from '../../../mock_data'; - -describe('IDE job description', () => { - let wrapper; - - beforeEach(() => { - wrapper = mount(Description, { - propsData: { - job: jobs[0], - }, - }); - }); - - it('renders job details', () => { - expect(wrapper.text()).toContain('#1'); - expect(wrapper.text()).toContain('test'); - }); - - it('renders CI icon', () => { - expect(wrapper.find('[data-testid="ci-icon"]').findComponent(GlIcon).exists()).toBe(true); - expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true); - }); - - it('renders bridge job details without the job link', () => { - wrapper = mount(Description, { - propsData: { - job: { ...jobs[0], path: undefined }, - }, - }); - - expect(wrapper.find('[data-testid="description-detail-link"]').exists()).toBe(false); - }); -}); diff --git a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js deleted file mode 100644 index 1f417be4a30..00000000000 --- a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import { GlIcon, GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue'; - -describe('IDE job log scroll button', () => { - let wrapper; - - const createComponent = (props) => { - wrapper = shallowMount(ScrollButton, { - propsData: { - direction: 'up', - disabled: false, - ...props, - }, - }); - }; - - describe.each` - direction | icon | title - ${'up'} | ${'scroll_up'} | ${'Scroll to top'} - ${'down'} | ${'scroll_down'} | ${'Scroll to bottom'} - `('for $direction direction', ({ direction, icon, title }) => { - beforeEach(() => createComponent({ direction })); - - it('returns proper icon name', () => { - expect(wrapper.findComponent(GlIcon).props('name')).toBe(icon); - }); - - it('returns proper title', () => { - expect(wrapper.attributes('title')).toBe(title); - }); - }); - - it('emits click event on click', () => { - createComponent(); - - wrapper.findComponent(GlButton).vm.$emit('click'); - expect(wrapper.emitted().click).toBeDefined(); - }); - - it('disables button when disabled is true', () => { - createComponent({ disabled: true }); - - expect(wrapper.findComponent(GlButton).attributes('disabled')).toBeDefined(); - }); -}); diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js deleted file mode 100644 index 334501bbca7..00000000000 --- a/spec/frontend/ide/components/jobs/detail_spec.js +++ /dev/null @@ -1,144 +0,0 @@ -import { nextTick } from 'vue'; -import { mount } from '@vue/test-utils'; - -import { TEST_HOST } from 'helpers/test_constants'; -import JobDetail from '~/ide/components/jobs/detail.vue'; -import { createStore } from '~/ide/stores'; -import { jobs } from '../../mock_data'; - -describe('IDE jobs detail view', () => { - let wrapper; - let store; - - const createComponent = () => { - store = createStore(); - - store.state.pipelines.detailJob = { - ...jobs[0], - isLoading: true, - output: 'testing', - rawPath: `${TEST_HOST}/raw`, - }; - - jest.spyOn(store, 'dispatch'); - store.dispatch.mockResolvedValue(); - - wrapper = mount(JobDetail, { store }); - }; - - const findBuildJobLog = () => wrapper.find('pre'); - const findScrollToBottomButton = () => wrapper.find('button[aria-label="Scroll to bottom"]'); - const findScrollToTopButton = () => wrapper.find('button[aria-label="Scroll to top"]'); - - beforeEach(() => { - createComponent(); - }); - - describe('mounted', () => { - const findJobOutput = () => wrapper.find('.bash'); - const findBuildLoaderAnimation = () => wrapper.find('.build-loader-animation'); - - it('calls fetchJobLogs', () => { - expect(store.dispatch).toHaveBeenCalledWith('pipelines/fetchJobLogs', undefined); - }); - - it('scrolls to bottom', () => { - expect(findBuildJobLog().element.scrollTo).toHaveBeenCalled(); - }); - - it('renders job output', () => { - expect(findJobOutput().text()).toContain('testing'); - }); - - it('renders empty message output', async () => { - store.state.pipelines.detailJob.output = ''; - await nextTick(); - - expect(findJobOutput().text()).toContain('No messages were logged'); - }); - - it('renders loading icon', () => { - expect(findBuildLoaderAnimation().exists()).toBe(true); - expect(findBuildLoaderAnimation().isVisible()).toBe(true); - }); - - it('hides output when loading', () => { - expect(findJobOutput().exists()).toBe(true); - expect(findJobOutput().isVisible()).toBe(false); - }); - - it('hide loading icon when isLoading is false', async () => { - store.state.pipelines.detailJob.isLoading = false; - await nextTick(); - - expect(findBuildLoaderAnimation().isVisible()).toBe(false); - }); - - it('resets detailJob when clicking header button', async () => { - await wrapper.find('.btn').trigger('click'); - - expect(store.dispatch).toHaveBeenCalledWith('pipelines/setDetailJob', null); - }); - - it('renders raw path link', () => { - expect(wrapper.find('.controllers-buttons').attributes('href')).toBe(`${TEST_HOST}/raw`); - }); - }); - - describe('scroll buttons', () => { - beforeEach(() => { - createComponent(); - }); - - it.each` - fnName | btnName | scrollPos | targetScrollPos - ${'scroll down'} | ${'down'} | ${0} | ${200} - ${'scroll up'} | ${'up'} | ${200} | ${0} - `('triggers $fnName when clicking $btnName button', async ({ scrollPos, targetScrollPos }) => { - jest.spyOn(findBuildJobLog().element, 'offsetHeight', 'get').mockReturnValue(0); - jest.spyOn(findBuildJobLog().element, 'scrollHeight', 'get').mockReturnValue(200); - jest.spyOn(findBuildJobLog().element, 'scrollTop', 'get').mockReturnValue(scrollPos); - findBuildJobLog().element.scrollTo.mockReset(); - - await findBuildJobLog().trigger('scroll'); // trigger button updates - - await wrapper.find('.controllers button:not(:disabled)').trigger('click'); - - expect(findBuildJobLog().element.scrollTo).toHaveBeenCalledWith(0, targetScrollPos); - }); - }); - - describe('scrolling build log', () => { - beforeEach(() => { - jest.spyOn(findBuildJobLog().element, 'offsetHeight', 'get').mockReturnValue(100); - jest.spyOn(findBuildJobLog().element, 'scrollHeight', 'get').mockReturnValue(200); - }); - - it('keeps scroll at bottom when already at the bottom', async () => { - jest.spyOn(findBuildJobLog().element, 'scrollTop', 'get').mockReturnValue(100); - - await findBuildJobLog().trigger('scroll'); - - expect(findScrollToBottomButton().attributes('disabled')).toBeDefined(); - expect(findScrollToTopButton().attributes('disabled')).toBeUndefined(); - }); - - it('keeps scroll at top when already at top', async () => { - jest.spyOn(findBuildJobLog().element, 'scrollTop', 'get').mockReturnValue(0); - - await findBuildJobLog().trigger('scroll'); - - expect(findScrollToBottomButton().attributes('disabled')).toBeUndefined(); - expect(findScrollToTopButton().attributes('disabled')).toBeDefined(); - }); - - it('resets scroll when not at top or bottom', async () => { - jest.spyOn(findBuildJobLog().element, 'scrollTop', 'get').mockReturnValue(10); - - await findBuildJobLog().trigger('scroll'); - - expect(findScrollToBottomButton().attributes('disabled')).toBeUndefined(); - expect(findScrollToTopButton().attributes('disabled')).toBeUndefined(); - }); - }); -}); diff --git a/spec/frontend/ide/components/jobs/item_spec.js b/spec/frontend/ide/components/jobs/item_spec.js deleted file mode 100644 index aa6fc5531dd..00000000000 --- a/spec/frontend/ide/components/jobs/item_spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { GlButton } from '@gitlab/ui'; - -import JobItem from '~/ide/components/jobs/item.vue'; -import { jobs } from '../../mock_data'; - -describe('IDE jobs item', () => { - const job = jobs[0]; - let wrapper; - - beforeEach(() => { - wrapper = mount(JobItem, { propsData: { job } }); - }); - - it('renders job details', () => { - expect(wrapper.text()).toContain(job.name); - expect(wrapper.text()).toContain(`#${job.id}`); - }); - - it('renders CI icon', () => { - expect(wrapper.find('[data-testid="ci-icon"]').exists()).toBe(true); - }); - - it('does not render view logs button if not started', async () => { - await wrapper.setProps({ - job: { - ...jobs[0], - started: false, - }, - }); - - expect(wrapper.findComponent(GlButton).exists()).toBe(false); - }); -}); diff --git a/spec/frontend/ide/components/jobs/list_spec.js b/spec/frontend/ide/components/jobs/list_spec.js deleted file mode 100644 index c466ef8e96f..00000000000 --- a/spec/frontend/ide/components/jobs/list_spec.js +++ /dev/null @@ -1,103 +0,0 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount, mount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import StageList from '~/ide/components/jobs/list.vue'; -import Stage from '~/ide/components/jobs/stage.vue'; - -Vue.use(Vuex); -const storeActions = { - fetchJobs: jest.fn(), - toggleStageCollapsed: jest.fn(), - setDetailJob: jest.fn(), -}; - -const store = new Vuex.Store({ - modules: { - pipelines: { - namespaced: true, - actions: storeActions, - }, - }, -}); - -describe('IDE stages list', () => { - let wrapper; - - const defaultProps = { - stages: [], - loading: false, - }; - - const stages = ['build', 'test', 'deploy'].map((name, id) => ({ - id, - name, - jobs: [], - status: { icon: 'status_success' }, - })); - - const createComponent = (props) => { - wrapper = shallowMount(StageList, { - propsData: { - ...defaultProps, - ...props, - }, - store, - }); - }; - - afterEach(() => { - Object.values(storeActions).forEach((actionMock) => actionMock.mockClear()); - }); - - it('renders loading icon when no stages & loading', () => { - createComponent({ loading: true, stages: [] }); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - - it('renders stages components for each stage', () => { - createComponent({ stages }); - expect(wrapper.findAllComponents(Stage).length).toBe(stages.length); - }); - - it('triggers fetchJobs action when stage emits fetch event', () => { - createComponent({ stages }); - wrapper.findComponent(Stage).vm.$emit('fetch'); - expect(storeActions.fetchJobs).toHaveBeenCalled(); - }); - - it('triggers toggleStageCollapsed action when stage emits toggleCollapsed event', () => { - createComponent({ stages }); - wrapper.findComponent(Stage).vm.$emit('toggleCollapsed'); - expect(storeActions.toggleStageCollapsed).toHaveBeenCalled(); - }); - - it('triggers setDetailJob action when stage emits clickViewLog event', () => { - createComponent({ stages }); - wrapper.findComponent(Stage).vm.$emit('clickViewLog'); - expect(storeActions.setDetailJob).toHaveBeenCalled(); - }); - - describe('integration tests', () => { - const findCardHeader = () => wrapper.find('.card-header'); - - beforeEach(() => { - wrapper = mount(StageList, { - propsData: { ...defaultProps, stages }, - store, - }); - }); - - it('calls toggleStageCollapsed when clicking stage header', () => { - findCardHeader().trigger('click'); - - expect(storeActions.toggleStageCollapsed).toHaveBeenCalledWith(expect.any(Object), 0); - }); - - it('calls fetchJobs when stage is mounted', () => { - expect(storeActions.fetchJobs.mock.calls.map(([, stage]) => stage)).toEqual(stages); - }); - }); -}); diff --git a/spec/frontend/ide/components/jobs/stage_spec.js b/spec/frontend/ide/components/jobs/stage_spec.js deleted file mode 100644 index 23ef92f9682..00000000000 --- a/spec/frontend/ide/components/jobs/stage_spec.js +++ /dev/null @@ -1,82 +0,0 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import Item from '~/ide/components/jobs/item.vue'; -import Stage from '~/ide/components/jobs/stage.vue'; -import { stages, jobs } from '../../mock_data'; - -describe('IDE pipeline stage', () => { - let wrapper; - const defaultProps = { - stage: { - ...stages[0], - id: 0, - dropdownPath: stages[0].dropdown_path, - jobs: [...jobs], - isLoading: false, - isCollapsed: false, - }, - }; - - const findHeader = () => wrapper.find('[data-testid="card-header"]'); - const findJobList = () => wrapper.find('[data-testid="job-list"]'); - const findStageTitle = () => wrapper.find('[data-testid="stage-title"]'); - - const createComponent = (props) => { - wrapper = shallowMount(Stage, { - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - it('emits fetch event when mounted', () => { - createComponent(); - expect(wrapper.emitted().fetch).toBeDefined(); - }); - - it('renders loading icon when no jobs and isLoading is true', () => { - createComponent({ - stage: { ...defaultProps.stage, isLoading: true, jobs: [] }, - }); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - - it('emits toggleCollaped event with stage id when clicking header', async () => { - const id = 5; - createComponent({ stage: { ...defaultProps.stage, id } }); - findHeader().trigger('click'); - - await nextTick(); - expect(wrapper.emitted().toggleCollapsed[0][0]).toBe(id); - }); - - it('emits clickViewLog entity with job', async () => { - const [job] = defaultProps.stage.jobs; - createComponent(); - wrapper.findAllComponents(Item).at(0).vm.$emit('clickViewLog', job); - await nextTick(); - expect(wrapper.emitted().clickViewLog[0][0]).toBe(job); - }); - - it('renders stage title', () => { - createComponent(); - expect(findStageTitle().isVisible()).toBe(true); - }); - - describe('when collapsed', () => { - beforeEach(() => { - createComponent({ stage: { ...defaultProps.stage, isCollapsed: true } }); - }); - - it('does not render job list', () => { - expect(findJobList().isVisible()).toBe(false); - }); - - it('sets border bottom class', () => { - expect(findHeader().classes('border-bottom-0')).toBe(true); - }); - }); -}); diff --git a/spec/frontend/ide/components/merge_requests/item_spec.js b/spec/frontend/ide/components/merge_requests/item_spec.js deleted file mode 100644 index 11f7f017932..00000000000 --- a/spec/frontend/ide/components/merge_requests/item_spec.js +++ /dev/null @@ -1,92 +0,0 @@ -import { mount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { describeSkipVue3, SkipReason } from 'helpers/vue3_conditional'; -import Item from '~/ide/components/merge_requests/item.vue'; -import { createRouter } from '~/ide/ide_router'; -import { createStore } from '~/ide/stores'; - -const TEST_ITEM = { - iid: 1, - projectPathWithNamespace: 'gitlab-org/gitlab-ce', - title: 'Merge request title', -}; - -const skipReason = new SkipReason({ - name: 'IDE merge request item', - reason: 'Legacy WebIDE is due for deletion', - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/508949', -}); -describeSkipVue3(skipReason, () => { - Vue.use(Vuex); - - let wrapper; - let store; - let router; - - const createComponent = (props = {}) => { - wrapper = mount(Item, { - propsData: { - item: { - ...TEST_ITEM, - }, - currentId: `${TEST_ITEM.iid}`, - currentProjectId: TEST_ITEM.projectPathWithNamespace, - ...props, - }, - router, - store, - }); - }; - const findIcon = () => wrapper.find('[data-testid="mobile-issue-close-icon"]'); - - beforeEach(() => { - store = createStore(); - router = createRouter(store); - }); - - describe('default', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders merge requests data', () => { - expect(wrapper.text()).toContain('Merge request title'); - expect(wrapper.text()).toContain('gitlab-org/gitlab-ce!1'); - }); - - it('renders link with href', () => { - const expectedHref = router.resolve( - `/project/${TEST_ITEM.projectPathWithNamespace}/merge_requests/${TEST_ITEM.iid}`, - ).href; - - expect(wrapper.element.tagName.toLowerCase()).toBe('a'); - expect(wrapper.attributes('href')).toBe(expectedHref); - }); - - it('renders icon if ID matches currentId', () => { - expect(findIcon().exists()).toBe(true); - }); - }); - - describe('with different currentId', () => { - beforeEach(() => { - createComponent({ currentId: `${TEST_ITEM.iid + 1}` }); - }); - - it('does not render icon', () => { - expect(findIcon().exists()).toBe(false); - }); - }); - - describe('with different project ID', () => { - beforeEach(() => { - createComponent({ currentProjectId: 'test/test' }); - }); - - it('does not render icon', () => { - expect(findIcon().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/ide/components/merge_requests/list_spec.js b/spec/frontend/ide/components/merge_requests/list_spec.js deleted file mode 100644 index 36a37f61df8..00000000000 --- a/spec/frontend/ide/components/merge_requests/list_spec.js +++ /dev/null @@ -1,188 +0,0 @@ -import { GlButton, GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { describeSkipVue3, SkipReason } from 'helpers/vue3_conditional'; -import Item from '~/ide/components/merge_requests/item.vue'; -import List from '~/ide/components/merge_requests/list.vue'; -import TokenedInput from '~/ide/components/shared/tokened_input.vue'; -import { mergeRequests as mergeRequestsMock } from '../../mock_data'; - -Vue.use(Vuex); - -const skipReason = new SkipReason({ - name: 'IDE merge requests list', - reason: 'Legacy WebIDE is due for deletion', - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/508949', -}); -describeSkipVue3(skipReason, () => { - let wrapper; - let fetchMergeRequestsMock; - - const findSearchTypeButtons = () => wrapper.findAllComponents(GlButton); - const findTokenedInput = () => wrapper.findComponent(TokenedInput); - - const createComponent = (state = {}) => { - const { mergeRequests = {}, ...restOfState } = state; - const fakeStore = new Vuex.Store({ - state: { - currentMergeRequestId: '1', - currentProjectId: 'project/main', - ...restOfState, - }, - modules: { - mergeRequests: { - namespaced: true, - state: { - isLoading: false, - mergeRequests: [], - ...mergeRequests, - }, - actions: { - fetchMergeRequests: fetchMergeRequestsMock, - }, - }, - }, - }); - - wrapper = shallowMount(List, { - store: fakeStore, - }); - }; - - beforeEach(() => { - fetchMergeRequestsMock = jest.fn(); - }); - - it('calls fetch on mounted', () => { - createComponent(); - expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), { - search: '', - type: '', - }); - }); - - it('renders loading icon when merge request is loading', () => { - createComponent({ mergeRequests: { isLoading: true } }); - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - - it('renders no search results text when search is not empty', async () => { - createComponent(); - findTokenedInput().vm.$emit('input', 'something'); - await nextTick(); - expect(wrapper.text()).toContain('No merge requests found'); - }); - - it('clicking on search type, sets currentSearchType and loads merge requests', async () => { - createComponent(); - findTokenedInput().vm.$emit('focus'); - - await nextTick(); - await findSearchTypeButtons().at(0).vm.$emit('click'); - - await nextTick(); - const searchType = wrapper.vm.$options.searchTypes[0]; - - expect(findTokenedInput().props('tokens')).toEqual([searchType]); - expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), { - type: searchType.type, - search: '', - }); - }); - - describe('with merge requests', () => { - let defaultStateWithMergeRequests; - - beforeAll(() => { - defaultStateWithMergeRequests = { - mergeRequests: { - isLoading: false, - mergeRequests: [ - { ...mergeRequestsMock[0], projectPathWithNamespace: 'gitlab-org/gitlab-foss' }, - ], - }, - }; - }); - - it('renders list', () => { - createComponent(defaultStateWithMergeRequests); - - expect(wrapper.findAllComponents(Item).length).toBe(1); - expect(wrapper.findComponent(Item).props('item')).toBe( - defaultStateWithMergeRequests.mergeRequests.mergeRequests[0], - ); - }); - - describe('when searching merge requests', () => { - it('calls `loadMergeRequests` on input in search field', async () => { - createComponent(defaultStateWithMergeRequests); - const input = findTokenedInput(); - input.vm.$emit('input', 'something'); - - await nextTick(); - expect(fetchMergeRequestsMock).toHaveBeenCalledWith(expect.any(Object), { - search: 'something', - type: '', - }); - }); - }); - }); - - describe('on search focus', () => { - let input; - - beforeEach(() => { - createComponent(); - input = findTokenedInput(); - }); - - describe('without search value', () => { - beforeEach(async () => { - input.vm.$emit('focus'); - await nextTick(); - }); - - it('shows search types', () => { - const buttons = findSearchTypeButtons(); - expect(buttons.wrappers.map((x) => x.text().trim())).toEqual( - wrapper.vm.$options.searchTypes.map((x) => x.label), - ); - }); - - it('hides search types when search changes', async () => { - input.vm.$emit('input', 'something'); - - await nextTick(); - expect(findSearchTypeButtons().exists()).toBe(false); - }); - - describe('with search type', () => { - beforeEach(async () => { - await findSearchTypeButtons().at(0).vm.$emit('click'); - - await nextTick(); - await input.vm.$emit('focus'); - await nextTick(); - }); - - it('does not show search types', () => { - expect(findSearchTypeButtons().exists()).toBe(false); - }); - }); - }); - - describe('with search value', () => { - beforeEach(async () => { - input.vm.$emit('input', 'something'); - input.vm.$emit('focus'); - await nextTick(); - }); - - it('does not show search types', () => { - expect(findSearchTypeButtons().exists()).toBe(false); - }); - }); - }); -}); diff --git a/spec/frontend/ide/components/nav_dropdown_button_spec.js b/spec/frontend/ide/components/nav_dropdown_button_spec.js deleted file mode 100644 index c469bd040be..00000000000 --- a/spec/frontend/ide/components/nav_dropdown_button_spec.js +++ /dev/null @@ -1,69 +0,0 @@ -import { trimText } from 'helpers/text_helper'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue'; -import { createStore } from '~/ide/stores'; - -describe('NavDropdownButton component', () => { - const TEST_BRANCH_ID = 'lorem-ipsum-dolar'; - const TEST_MR_ID = '12345'; - let wrapper; - - const createComponent = ({ props = {}, state = {} } = {}) => { - const store = createStore(); - store.replaceState(state); - wrapper = shallowMountExtended(NavDropdownButton, { propsData: props, store }); - }; - - const findMRIcon = () => wrapper.findByTestId('merge-request-icon'); - const findBranchIcon = () => wrapper.findByTestId('branch-icon'); - - describe('normal', () => { - it('renders empty placeholders, if state is falsey', () => { - createComponent(); - - expect(trimText(wrapper.text())).toBe('- -'); - }); - - it('renders branch name, if state has currentBranchId', () => { - createComponent({ state: { currentBranchId: TEST_BRANCH_ID } }); - - expect(trimText(wrapper.text())).toBe(`${TEST_BRANCH_ID} -`); - }); - - it('renders mr id, if state has currentMergeRequestId', () => { - createComponent({ state: { currentMergeRequestId: TEST_MR_ID } }); - - expect(trimText(wrapper.text())).toBe(`- !${TEST_MR_ID}`); - }); - - it('renders branch and mr, if state has both', () => { - createComponent({ - state: { currentBranchId: TEST_BRANCH_ID, currentMergeRequestId: TEST_MR_ID }, - }); - - expect(trimText(wrapper.text())).toBe(`${TEST_BRANCH_ID} !${TEST_MR_ID}`); - }); - - it('shows icons', () => { - createComponent(); - - expect(findBranchIcon().exists()).toBe(true); - expect(findMRIcon().exists()).toBe(true); - }); - }); - - describe('when showMergeRequests=false', () => { - beforeEach(() => { - createComponent({ props: { showMergeRequests: false } }); - }); - - it('shows single empty placeholder, if state is falsey', () => { - expect(trimText(wrapper.text())).toBe('-'); - }); - - it('shows only branch icon', () => { - expect(findBranchIcon().exists()).toBe(true); - expect(findMRIcon().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/ide/components/nav_dropdown_spec.js b/spec/frontend/ide/components/nav_dropdown_spec.js deleted file mode 100644 index 794aaba6d01..00000000000 --- a/spec/frontend/ide/components/nav_dropdown_spec.js +++ /dev/null @@ -1,89 +0,0 @@ -import { mount } from '@vue/test-utils'; -import $ from 'jquery'; -import { nextTick } from 'vue'; -import NavDropdown from '~/ide/components/nav_dropdown.vue'; -import { PERMISSION_READ_MR } from '~/ide/constants'; -import { createStore } from '~/ide/stores'; - -const TEST_PROJECT_ID = 'lorem-ipsum'; - -describe('IDE NavDropdown', () => { - let store; - let wrapper; - - beforeEach(() => { - store = createStore(); - Object.assign(store.state, { - currentProjectId: TEST_PROJECT_ID, - currentBranchId: 'main', - projects: { - [TEST_PROJECT_ID]: { - userPermissions: { - [PERMISSION_READ_MR]: true, - }, - branches: { - main: { id: 'main' }, - }, - }, - }, - }); - jest.spyOn(store, 'dispatch').mockImplementation(() => {}); - }); - - const createComponent = () => { - wrapper = mount(NavDropdown, { - store, - }); - }; - - const findIcon = (name) => wrapper.find(`[data-testid="${name}-icon"]`); - const findMRIcon = () => findIcon('merge-request'); - const findNavForm = () => wrapper.find('.ide-nav-form'); - const showDropdown = () => { - $(wrapper.vm.$el).trigger('show.bs.dropdown'); - }; - const hideDropdown = () => { - $(wrapper.vm.$el).trigger('hide.bs.dropdown'); - }; - - describe('default', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders nothing initially', () => { - expect(findNavForm().exists()).toBe(false); - }); - - it('renders nav form when show.bs.dropdown', async () => { - showDropdown(); - - await nextTick(); - expect(findNavForm().exists()).toBe(true); - }); - - it('destroys nav form when closed', async () => { - showDropdown(); - hideDropdown(); - - await nextTick(); - expect(findNavForm().exists()).toBe(false); - }); - - it('renders merge request icon', () => { - expect(findMRIcon().exists()).toBe(true); - }); - }); - - describe('when user cannot read merge requests', () => { - beforeEach(() => { - store.state.projects[TEST_PROJECT_ID].userPermissions = {}; - - createComponent(); - }); - - it('does not render merge requests', () => { - expect(findMRIcon().exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/ide/components/new_dropdown/button_spec.js b/spec/frontend/ide/components/new_dropdown/button_spec.js deleted file mode 100644 index bfd5cdf7263..00000000000 --- a/spec/frontend/ide/components/new_dropdown/button_spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import { mount } from '@vue/test-utils'; -import Button from '~/ide/components/new_dropdown/button.vue'; - -describe('IDE new entry dropdown button component', () => { - let wrapper; - - const createComponent = (props = {}) => { - wrapper = mount(Button, { - propsData: { - label: 'Testing', - icon: 'doc-new', - ...props, - }, - }); - }; - - it('renders button with label', () => { - createComponent(); - - expect(wrapper.text()).toContain('Testing'); - }); - - it('renders icon', () => { - createComponent(); - - expect(wrapper.find('[data-testid="doc-new-icon"]').exists()).toBe(true); - }); - - it('emits click event', async () => { - createComponent(); - - await wrapper.trigger('click'); - - expect(wrapper.emitted('click')).toHaveLength(1); - }); - - it('hides label if showLabel is false', () => { - createComponent({ showLabel: false }); - - expect(wrapper.text()).not.toContain('Testing'); - }); - - describe('tooltip title', () => { - it('returns empty string when showLabel is true', () => { - createComponent({ showLabel: true }); - - expect(wrapper.attributes('title')).toBe(''); - }); - - it('returns label', () => { - createComponent({ showLabel: false }); - - expect(wrapper.attributes('title')).toBe('Testing'); - }); - }); -}); diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js deleted file mode 100644 index a9b8face862..00000000000 --- a/spec/frontend/ide/components/new_dropdown/index_spec.js +++ /dev/null @@ -1,95 +0,0 @@ -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { describeSkipVue3, SkipReason } from 'helpers/vue3_conditional'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import NewDropdown from '~/ide/components/new_dropdown/index.vue'; -import Button from '~/ide/components/new_dropdown/button.vue'; -import Modal from '~/ide/components/new_dropdown/modal.vue'; -import { stubComponent } from 'helpers/stub_component'; - -Vue.use(Vuex); - -const skipReason = new SkipReason({ - name: 'new dropdown component', - reason: 'Legacy WebIDE is due for deletion', - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/508949', -}); -describeSkipVue3(skipReason, () => { - let wrapper; - const openMock = jest.fn(); - const deleteEntryMock = jest.fn(); - - const findAllButtons = () => wrapper.findAllComponents(Button); - - const mountComponent = (props = {}) => { - const fakeStore = () => { - return new Vuex.Store({ - actions: { - deleteEntry: deleteEntryMock, - }, - }); - }; - - wrapper = mountExtended(NewDropdown, { - store: fakeStore(), - propsData: { - branch: 'main', - path: '', - mouseOver: false, - type: 'tree', - ...props, - }, - stubs: { - NewModal: stubComponent(Modal, { - methods: { - open: openMock, - }, - }), - }, - }); - }; - - beforeEach(() => { - mountComponent(); - }); - - it('renders new file, upload and new directory links', () => { - expect(findAllButtons().at(0).text()).toBe('New file'); - expect(findAllButtons().at(1).text()).toBe('Upload file'); - expect(findAllButtons().at(2).text()).toBe('New directory'); - }); - - describe('createNewItem', () => { - it('opens modal for a blob when new file is clicked', () => { - findAllButtons().at(0).vm.$emit('click'); - - expect(openMock).toHaveBeenCalledWith('blob', ''); - }); - - it('opens modal for a tree when new directory is clicked', () => { - findAllButtons().at(2).vm.$emit('click'); - - expect(openMock).toHaveBeenCalledWith('tree', ''); - }); - }); - - describe('isOpen', () => { - it('scrolls dropdown into view', async () => { - const dropdownMenu = wrapper.findByTestId('dropdown-menu'); - const scrollIntoViewSpy = jest.spyOn(dropdownMenu.element, 'scrollIntoView'); - - await wrapper.setProps({ isOpen: true }); - - expect(scrollIntoViewSpy).toHaveBeenCalledWith({ block: 'nearest' }); - }); - }); - - describe('delete entry', () => { - it('calls delete action', () => { - findAllButtons().at(4).trigger('click'); - - expect(deleteEntryMock).toHaveBeenCalledWith(expect.anything(), ''); - }); - }); -}); diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js deleted file mode 100644 index 36c3d323e63..00000000000 --- a/spec/frontend/ide/components/new_dropdown/modal_spec.js +++ /dev/null @@ -1,418 +0,0 @@ -import { GlButton, GlModal } from '@gitlab/ui'; -import { nextTick } from 'vue'; -import { createAlert } from '~/alert'; -import Modal from '~/ide/components/new_dropdown/modal.vue'; -import { createStore } from '~/ide/stores'; -import { stubComponent } from 'helpers/stub_component'; -import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import { createEntriesFromPaths } from '../../helpers'; - -jest.mock('~/alert'); - -const NEW_NAME = 'babar'; - -describe('new file modal component', () => { - const showModal = jest.fn(); - const toggleModal = jest.fn(); - - let store; - let wrapper; - - const findForm = () => wrapper.findByTestId('file-name-form'); - const findGlModal = () => wrapper.findComponent(GlModal); - const findInput = () => wrapper.findByTestId('file-name-field'); - const findTemplateButtons = () => wrapper.findAllComponents(GlButton); - const findTemplateButtonsModel = () => - findTemplateButtons().wrappers.map((x) => ({ - text: x.text(), - variant: x.props('variant'), - category: x.props('category'), - })); - - const open = (type, path) => { - // TODO: This component can not be passed props - // We have to interact with the open() method? - wrapper.vm.open(type, path); - }; - const triggerSubmitForm = () => { - findForm().trigger('submit'); - }; - const triggerSubmitModal = () => { - findGlModal().vm.$emit('primary'); - }; - const triggerCancel = () => { - findGlModal().vm.$emit('cancel'); - }; - - const mountComponent = () => { - const GlModalStub = stubComponent(GlModal); - jest.spyOn(GlModalStub.methods, 'show').mockImplementation(showModal); - jest.spyOn(GlModalStub.methods, 'toggle').mockImplementation(toggleModal); - - wrapper = shallowMountExtended(Modal, { - store, - stubs: { - GlModal: GlModalStub, - }, - // We need to attach to document for "focus" to work - attachTo: document.body, - }); - }; - - beforeEach(() => { - store = createStore(); - - Object.assign( - store.state.entries, - createEntriesFromPaths([ - 'README.md', - 'src', - 'src/deleted.js', - 'src/parent_dir', - 'src/parent_dir/foo.js', - ]), - ); - Object.assign(store.state.entries['src/deleted.js'], { deleted: true }); - - jest.spyOn(store, 'dispatch').mockImplementation(); - }); - - afterEach(() => { - store = null; - document.body.innerHTML = ''; - }); - - describe('default', () => { - beforeEach(async () => { - mountComponent(); - - // Not necessarily needed, but used to ensure that nothing extra is happening after the tick - await nextTick(); - }); - - it('renders modal', () => { - expect(findGlModal().props()).toMatchObject({ - actionCancel: { - attributes: { variant: 'default' }, - text: 'Cancel', - }, - actionPrimary: { - attributes: { variant: 'confirm' }, - text: 'Create file', - }, - actionSecondary: null, - size: 'lg', - modalId: 'ide-new-entry', - title: 'Create new file', - }); - }); - - it('renders name label', () => { - expect(wrapper.find('label').text()).toBe('Name'); - }); - - it('renders template buttons', () => { - const actual = findTemplateButtonsModel(); - - expect(actual.length).toBeGreaterThan(0); - expect(actual).toEqual( - store.getters['fileTemplates/templateTypes'].map((template) => ({ - category: 'secondary', - text: template.name, - variant: 'dashed', - })), - ); - }); - - // These negative ".not.toHaveBeenCalled" assertions complement the positive "toHaveBeenCalled" - // assertions that show up later in this spec. Without these, we're not guaranteed the "act" - // actually caused the change in behavior. - it('does not dispatch actions by default', () => { - expect(store.dispatch).not.toHaveBeenCalled(); - }); - - it('does not trigger modal by default', () => { - expect(showModal).not.toHaveBeenCalled(); - expect(toggleModal).not.toHaveBeenCalled(); - }); - - it('does not focus input by default', () => { - expect(document.activeElement).toBe(document.body); - }); - }); - - describe.each` - entryType | path | modalTitle | btnTitle | showsFileTemplates | inputValue | inputPlaceholder - ${'tree'} | ${''} | ${'Create new directory'} | ${'Create directory'} | ${false} | ${''} | ${'dir/'} - ${'blob'} | ${''} | ${'Create new file'} | ${'Create file'} | ${true} | ${''} | ${'dir/file_name'} - ${'blob'} | ${'foo/bar'} | ${'Create new file'} | ${'Create file'} | ${true} | ${'foo/bar/'} | ${'dir/file_name'} - `( - 'when opened as $entryType with path "$path"', - ({ - entryType, - path, - modalTitle, - btnTitle, - showsFileTemplates, - inputValue, - inputPlaceholder, - }) => { - beforeEach(async () => { - mountComponent(); - - open(entryType, path); - - await nextTick(); - }); - - it('sets modal props', () => { - expect(findGlModal().props()).toMatchObject({ - title: modalTitle, - actionPrimary: { - attributes: { variant: 'confirm' }, - text: btnTitle, - }, - }); - }); - - it('sets input attributes', () => { - expect(findInput().element.value).toBe(inputValue); - expect(findInput().attributes('placeholder')).toBe(inputPlaceholder); - }); - - it(`shows file templates: ${showsFileTemplates}`, () => { - const actual = findTemplateButtonsModel().length > 0; - - expect(actual).toBe(showsFileTemplates); - }); - - it('shows modal', () => { - expect(showModal).toHaveBeenCalled(); - }); - - it('focus on input', () => { - expect(document.activeElement).toBe(findInput().element); - }); - - it('resets when canceled', async () => { - triggerCancel(); - - await nextTick(); - - // Resets input value - expect(findInput().element.value).toBe(''); - // Resets to blob mode - expect(findGlModal().props('title')).toBe('Create new file'); - }); - }, - ); - - describe.each` - modalType | name | expectedName - ${'blob'} | ${'foo/bar.js'} | ${'foo/bar.js'} - ${'blob'} | ${'foo /bar.js'} | ${'foo/bar.js'} - ${'tree'} | ${'foo/dir'} | ${'foo/dir'} - ${'tree'} | ${'foo /dir'} | ${'foo/dir'} - `('when submitting as $modalType with "$name"', ({ modalType, name, expectedName }) => { - describe('when using the modal primary button', () => { - beforeEach(async () => { - mountComponent(); - - open(modalType, ''); - await nextTick(); - - findInput().setValue(name); - triggerSubmitModal(); - }); - - it('triggers createTempEntry action', () => { - expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { - name: expectedName, - type: modalType, - }); - }); - }); - - describe('when triggering form submit (pressing enter)', () => { - beforeEach(async () => { - mountComponent(); - - open(modalType, ''); - await nextTick(); - - findInput().setValue(name); - triggerSubmitForm(); - }); - - it('triggers createTempEntry action', () => { - expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { - name: expectedName, - type: modalType, - }); - }); - }); - }); - - describe('when creating from template type', () => { - beforeEach(async () => { - mountComponent(); - - open('blob', 'some_dir'); - - await nextTick(); - - // Set input, then trigger button - findInput().setValue('some_dir/foo.js'); - findTemplateButtons().at(1).vm.$emit('click'); - }); - - it('triggers createTempEntry action', () => { - const { name: expectedName } = store.getters['fileTemplates/templateTypes'][1]; - - expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { - name: `some_dir/${expectedName}`, - type: 'blob', - }); - }); - - it('toggles modal', () => { - expect(toggleModal).toHaveBeenCalled(); - }); - }); - - describe.each` - origPath | title | inputValue | inputSelectionStart - ${'src/parent_dir'} | ${'Rename folder'} | ${'src/parent_dir'} | ${'src/'.length} - ${'README.md'} | ${'Rename file'} | ${'README.md'} | ${0} - `('when renaming for $origPath', ({ origPath, title, inputValue, inputSelectionStart }) => { - beforeEach(async () => { - mountComponent(); - - open('rename', origPath); - - await nextTick(); - }); - - it('sets modal props for renaming', () => { - expect(findGlModal().props()).toMatchObject({ - title, - actionPrimary: { - attributes: { variant: 'confirm' }, - text: title, - }, - }); - }); - - it('sets input value', () => { - expect(findInput().element.value).toBe(inputValue); - }); - - it(`does not show file templates`, () => { - expect(findTemplateButtonsModel()).toHaveLength(0); - }); - - it('shows modal when renaming', () => { - expect(showModal).toHaveBeenCalled(); - }); - - it('focus on input when renaming', () => { - expect(document.activeElement).toBe(findInput().element); - }); - - it('selects name part of the input', () => { - expect(findInput().element.selectionStart).toBe(inputSelectionStart); - expect(findInput().element.selectionEnd).toBe(origPath.length); - }); - - describe('when renames is submitted successfully', () => { - describe('when using the modal primary button', () => { - beforeEach(() => { - findInput().setValue(NEW_NAME); - triggerSubmitModal(); - }); - - it('dispatches renameEntry event', () => { - expect(store.dispatch).toHaveBeenCalledWith('renameEntry', { - path: origPath, - parentPath: '', - name: NEW_NAME, - }); - }); - - it('does not trigger alert', () => { - expect(createAlert).not.toHaveBeenCalled(); - }); - }); - - describe('when triggering form submit (pressing enter)', () => { - beforeEach(() => { - findInput().setValue(NEW_NAME); - triggerSubmitForm(); - }); - - it('dispatches renameEntry event', () => { - expect(store.dispatch).toHaveBeenCalledWith('renameEntry', { - path: origPath, - parentPath: '', - name: NEW_NAME, - }); - }); - - it('does not trigger alert', () => { - expect(createAlert).not.toHaveBeenCalled(); - }); - }); - }); - }); - - describe('when renaming and file already exists', () => { - beforeEach(async () => { - mountComponent(); - - open('rename', 'src/parent_dir'); - - await nextTick(); - - // Set to something that already exists! - findInput().setValue('src'); - triggerSubmitModal(); - }); - - it('creates alert', () => { - expect(createAlert).toHaveBeenCalledWith({ - message: 'The name "src" is already taken in this directory.', - fadeTransition: false, - addBodyClass: true, - }); - }); - - it('does not dispatch event', () => { - expect(store.dispatch).not.toHaveBeenCalled(); - }); - }); - - describe('when renaming and file has been deleted', () => { - beforeEach(async () => { - mountComponent(); - - open('rename', 'src/parent_dir/foo.js'); - - await nextTick(); - - findInput().setValue('src/deleted.js'); - triggerSubmitModal(); - }); - - it('does not create alert', () => { - expect(createAlert).not.toHaveBeenCalled(); - }); - - it('dispatches event', () => { - expect(store.dispatch).toHaveBeenCalledWith('renameEntry', { - path: 'src/parent_dir/foo.js', - name: 'deleted.js', - parentPath: 'src', - }); - }); - }); -}); diff --git a/spec/frontend/ide/components/new_dropdown/upload_spec.js b/spec/frontend/ide/components/new_dropdown/upload_spec.js deleted file mode 100644 index 883b365c99a..00000000000 --- a/spec/frontend/ide/components/new_dropdown/upload_spec.js +++ /dev/null @@ -1,92 +0,0 @@ -import { mount } from '@vue/test-utils'; -import Upload from '~/ide/components/new_dropdown/upload.vue'; -import waitForPromises from 'helpers/wait_for_promises'; - -describe('new dropdown upload', () => { - let wrapper; - - function createComponent() { - wrapper = mount(Upload, { - propsData: { - path: '', - }, - }); - } - - const uploadFile = (file) => { - const input = wrapper.find('input[type="file"]'); - Object.defineProperty(input.element, 'files', { value: [file] }); - input.trigger('change', file); - }; - - const waitForFileToLoad = async () => { - await waitForPromises(); - return waitForPromises(); - }; - - beforeEach(() => { - createComponent(); - }); - - describe('readFile', () => { - beforeEach(() => { - jest.spyOn(FileReader.prototype, 'readAsDataURL').mockImplementation(() => {}); - }); - - it('calls readAsDataURL for all files', () => { - const file = { - type: 'images/png', - }; - - uploadFile(file); - - expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file); - }); - }); - - describe('createFile', () => { - const textFile = new File(['plain text'], 'textFile', { type: 'test/mime-text' }); - const binaryFile = new File(['😺'], 'binaryFile', { type: 'test/mime-binary' }); - - beforeEach(() => { - jest.spyOn(FileReader.prototype, 'readAsText'); - }); - - it('calls readAsText and creates file in plain text (without encoding) if the file content is plain text', async () => { - uploadFile(textFile); - - // Text file has an additional load, so need to wait twice - await waitForFileToLoad(); - await waitForFileToLoad(); - - expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(textFile); - expect(wrapper.emitted('create')[0]).toStrictEqual([ - { - name: textFile.name, - type: 'blob', - content: 'plain text', - rawPath: '', - mimeType: 'test/mime-text', - }, - ]); - }); - - it('creates a blob URL for the content if binary', async () => { - uploadFile(binaryFile); - - await waitForFileToLoad(); - - expect(FileReader.prototype.readAsText).not.toHaveBeenCalled(); - - expect(wrapper.emitted('create')[0]).toStrictEqual([ - { - name: binaryFile.name, - type: 'blob', - content: '😺', // '😺' - rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b', - mimeType: 'test/mime-binary', - }, - ]); - }); - }); -}); diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js deleted file mode 100644 index 174e62550d5..00000000000 --- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js +++ /dev/null @@ -1,139 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue'; -import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue'; -import { createStore } from '~/ide/stores'; -import paneModule from '~/ide/stores/modules/pane'; - -Vue.use(Vuex); - -describe('ide/components/panes/collapsible_sidebar.vue', () => { - let wrapper; - let store; - - const width = 350; - const fakeComponentName = 'fake-component'; - - const createComponent = (props) => { - wrapper = shallowMount(CollapsibleSidebar, { - store, - propsData: { - extensionTabs: [], - side: 'right', - width, - ...props, - }, - }); - }; - - const findSidebarNav = () => wrapper.findComponent(IdeSidebarNav); - - beforeEach(() => { - store = createStore(); - store.registerModule('leftPane', paneModule()); - jest.spyOn(store, 'dispatch').mockImplementation(); - }); - - describe('with a tab', () => { - const FakeComponent = Vue.component(fakeComponentName, { - render: () => null, - }); - - const fakeView = { - name: fakeComponentName, - keepAlive: true, - component: FakeComponent, - }; - - const extensionTabs = [ - { - show: true, - title: fakeComponentName, - views: [fakeView], - icon: 'text-description', - buttonClasses: ['button-class-1', 'button-class-2'], - }, - ]; - - describe.each` - side - ${'left'} - ${'right'} - `('when side=$side', ({ side }) => { - beforeEach(() => { - createComponent({ extensionTabs, side }); - }); - - it('correctly renders side specific attributes', () => { - expect(wrapper.classes()).toContain('multi-file-commit-panel'); - expect(wrapper.classes()).toContain(`ide-${side}-sidebar`); - expect(wrapper.find('.multi-file-commit-panel-inner')).not.toBe(null); - expect(wrapper.find(`.ide-${side}-sidebar-${fakeComponentName}`)).not.toBe(null); - expect(findSidebarNav().props('side')).toBe(side); - }); - - it('when sidebar emits open, dispatch open', () => { - const view = 'lorem-view'; - - findSidebarNav().vm.$emit('open', view); - - expect(store.dispatch).toHaveBeenCalledWith(`${side}Pane/open`, view); - }); - - it('when sidebar emits close, dispatch toggleOpen', () => { - findSidebarNav().vm.$emit('close'); - - expect(store.dispatch).toHaveBeenCalledWith(`${side}Pane/toggleOpen`); - }); - }); - - describe('when side bar is rendered initially', () => { - it('nothing is dispatched', () => { - createComponent({ extensionTabs }); - expect(store.dispatch).not.toHaveBeenCalled(); - }); - }); - - describe.each` - isOpen - ${true} - ${false} - `('when isOpen=$isOpen', ({ isOpen }) => { - beforeEach(() => { - store.state.rightPane.isOpen = isOpen; - store.state.rightPane.currentView = fakeComponentName; - - createComponent({ extensionTabs }); - }); - - it(`tab view is shown=${isOpen}`, () => { - expect(wrapper.find('.js-tab-view').exists()).toBe(isOpen); - }); - - it('renders sidebar nav', () => { - expect(findSidebarNav().props()).toEqual({ - tabs: extensionTabs, - side: 'right', - currentView: fakeComponentName, - isOpen, - }); - }); - }); - - describe('with initOpenView that does not exist', () => { - it('nothing is dispatched', () => { - createComponent({ extensionTabs, initOpenView: 'does-not-exist' }); - expect(store.dispatch).not.toHaveBeenCalled(); - }); - }); - - describe('with initOpenView that does exist', () => { - it('dispatches open with view on create', () => { - createComponent({ extensionTabs, initOpenView: fakeView.name }); - expect(store.dispatch).toHaveBeenCalledWith('rightPane/open', fakeView); - }); - }); - }); -}); diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js deleted file mode 100644 index fc75eadbfe0..00000000000 --- a/spec/frontend/ide/components/panes/right_spec.js +++ /dev/null @@ -1,99 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue'; -import RightPane from '~/ide/components/panes/right.vue'; -import { rightSidebarViews } from '~/ide/constants'; -import { createStore } from '~/ide/stores'; -import extendStore from '~/ide/stores/extend'; - -Vue.use(Vuex); - -describe('ide/components/panes/right.vue', () => { - let wrapper; - let store; - - const createComponent = (props) => { - extendStore(store, document.createElement('div')); - - wrapper = shallowMount(RightPane, { - store, - propsData: { - ...props, - }, - }); - }; - - beforeEach(() => { - store = createStore(); - }); - - describe('default', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders collapsible-sidebar', () => { - expect(wrapper.findComponent(CollapsibleSidebar).props()).toMatchObject({ - side: 'right', - }); - }); - }); - - describe('pipelines tab', () => { - it('is always shown', () => { - createComponent(); - - expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - show: true, - title: 'Pipelines', - views: expect.arrayContaining([ - expect.objectContaining({ - name: rightSidebarViews.pipelines.name, - }), - expect.objectContaining({ - name: rightSidebarViews.jobsDetail.name, - }), - ]), - }), - ]), - ); - }); - }); - - describe('terminal tab', () => { - beforeEach(() => { - createComponent(); - }); - - it('adds terminal tab', async () => { - store.state.terminal.isVisible = true; - - await nextTick(); - expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - show: true, - title: 'Terminal', - }), - ]), - ); - }); - - it('hides terminal tab when not visible', () => { - store.state.terminal.isVisible = false; - - expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - show: false, - title: 'Terminal', - }), - ]), - ); - }); - }); -}); diff --git a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap b/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap deleted file mode 100644 index 06d999560ad..00000000000 --- a/spec/frontend/ide/components/pipelines/__snapshots__/list_spec.js.snap +++ /dev/null @@ -1,13 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`IDE pipelines list when loaded renders empty state when no latestPipeline 1`] = ` -
-
- -
-
-`; diff --git a/spec/frontend/ide/components/pipelines/empty_state_spec.js b/spec/frontend/ide/components/pipelines/empty_state_spec.js deleted file mode 100644 index bc52a8af70b..00000000000 --- a/spec/frontend/ide/components/pipelines/empty_state_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import { GlEmptyState } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import EmptyState from '~/ide/components/pipelines/empty_state.vue'; -import { createStore } from '~/ide/stores'; - -const TEST_PIPELINES_EMPTY_STATE_SVG_PATH = 'illustrations/test/pipelines.svg'; - -describe('~/ide/components/pipelines/empty_state.vue', () => { - let store; - let wrapper; - - const createComponent = () => { - wrapper = shallowMount(EmptyState, { - store, - }); - }; - - beforeEach(() => { - store = createStore(); - store.dispatch('setEmptyStateSvgs', { - pipelinesEmptyStateSvgPath: TEST_PIPELINES_EMPTY_STATE_SVG_PATH, - }); - }); - - describe('default', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders empty state', () => { - expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ - title: EmptyState.i18n.title, - description: EmptyState.i18n.description, - primaryButtonText: EmptyState.i18n.primaryButtonText, - primaryButtonLink: '/help/ci/quick_start/_index.md', - svgPath: TEST_PIPELINES_EMPTY_STATE_SVG_PATH, - }); - }); - }); -}); diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js deleted file mode 100644 index eb5043dbea9..00000000000 --- a/spec/frontend/ide/components/pipelines/list_spec.js +++ /dev/null @@ -1,178 +0,0 @@ -import { GlLoadingIcon, GlTab } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { pipelines } from 'jest/ide/mock_data'; -import JobsList from '~/ide/components/jobs/list.vue'; -import List from '~/ide/components/pipelines/list.vue'; -import EmptyState from '~/ide/components/pipelines/empty_state.vue'; -import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue'; - -Vue.use(Vuex); - -describe('IDE pipelines list', () => { - let wrapper; - - const defaultPipelinesState = { - stages: [], - failedStages: [], - isLoadingJobs: false, - }; - - const fetchLatestPipelineMock = jest.fn(); - const failedStagesGetterMock = jest.fn().mockReturnValue([]); - const fakeProjectPath = 'alpha/beta'; - - const createStore = (rootState, pipelinesState) => { - return new Vuex.Store({ - getters: { - currentProject: () => ({ web_url: 'some/url ', path_with_namespace: fakeProjectPath }), - }, - state: { - ...rootState, - }, - modules: { - pipelines: { - namespaced: true, - state: { - ...defaultPipelinesState, - ...pipelinesState, - }, - actions: { - fetchLatestPipeline: fetchLatestPipelineMock, - }, - getters: { - jobsCount: () => 1, - failedJobsCount: () => 1, - failedStages: failedStagesGetterMock, - pipelineFailed: () => false, - }, - }, - }, - }); - }; - - const createComponent = (state = {}, pipelinesState = {}) => { - wrapper = shallowMount(List, { - store: createStore(state, pipelinesState), - }); - }; - - it('fetches latest pipeline', () => { - createComponent(); - - expect(fetchLatestPipelineMock).toHaveBeenCalled(); - }); - - describe('when loading', () => { - let defaultPipelinesLoadingState; - - beforeAll(() => { - defaultPipelinesLoadingState = { - isLoadingPipeline: true, - }; - }); - - it('does not render when pipeline has loaded before', () => { - createComponent( - {}, - { - ...defaultPipelinesLoadingState, - hasLoadedPipeline: true, - }, - ); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); - }); - - it('renders loading state', () => { - createComponent( - {}, - { - ...defaultPipelinesLoadingState, - hasLoadedPipeline: false, - }, - ); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - }); - - describe('when loaded', () => { - let defaultPipelinesLoadedState; - - beforeAll(() => { - defaultPipelinesLoadedState = { - isLoadingPipeline: false, - hasLoadedPipeline: true, - }; - }); - - it('renders empty state when no latestPipeline', () => { - createComponent({}, { ...defaultPipelinesLoadedState, latestPipeline: null }); - - expect(wrapper.findComponent(EmptyState).exists()).toBe(true); - expect(wrapper.element).toMatchSnapshot(); - }); - - describe('with latest pipeline loaded', () => { - let withLatestPipelineState; - - beforeAll(() => { - withLatestPipelineState = { - ...defaultPipelinesLoadedState, - latestPipeline: pipelines[0], - }; - }); - - it('renders ci icon', () => { - createComponent({}, withLatestPipelineState); - expect(wrapper.findComponent(CiIcon).exists()).toBe(true); - }); - - it('renders pipeline data', () => { - createComponent({}, withLatestPipelineState); - - expect(wrapper.text()).toContain('#1'); - }); - - it('renders list of jobs', () => { - const stages = []; - const isLoadingJobs = true; - createComponent({}, { ...withLatestPipelineState, stages, isLoadingJobs }); - - const jobProps = wrapper.findAllComponents(GlTab).at(0).findComponent(JobsList).props(); - expect(jobProps.stages).toBe(stages); - expect(jobProps.loading).toBe(isLoadingJobs); - }); - - it('renders list of failed jobs', () => { - const failedStages = []; - failedStagesGetterMock.mockReset().mockReturnValue(failedStages); - const isLoadingJobs = true; - createComponent({}, { ...withLatestPipelineState, isLoadingJobs }); - - const jobProps = wrapper.findAllComponents(GlTab).at(1).findComponent(JobsList).props(); - expect(jobProps.stages).toBe(failedStages); - expect(jobProps.loading).toBe(isLoadingJobs); - }); - - describe('with YAML error', () => { - it('renders YAML error', () => { - const yamlError = 'test yaml error'; - createComponent( - {}, - { - ...defaultPipelinesLoadedState, - latestPipeline: { ...pipelines[0], yamlError }, - }, - ); - - expect(wrapper.text()).toContain('Unable to create pipeline'); - expect(wrapper.text()).toContain(yamlError); - }); - }); - }); - }); -}); diff --git a/spec/frontend/ide/components/repo_commit_section_spec.js b/spec/frontend/ide/components/repo_commit_section_spec.js deleted file mode 100644 index 3dd9ae1285d..00000000000 --- a/spec/frontend/ide/components/repo_commit_section_spec.js +++ /dev/null @@ -1,157 +0,0 @@ -import { mount } from '@vue/test-utils'; -import { keepAlive } from 'helpers/keep_alive_component_helper'; -import EmptyState from '~/ide/components/commit_sidebar/empty_state.vue'; -import RepoCommitSection from '~/ide/components/repo_commit_section.vue'; -import { stageKeys } from '~/ide/constants'; -import { createRouter } from '~/ide/ide_router'; -import { createStore } from '~/ide/stores'; -import { file } from '../helpers'; - -const TEST_NO_CHANGES_SVG = 'nochangessvg'; - -describe('RepoCommitSection', () => { - let wrapper; - let router; - let store; - - function createComponent() { - wrapper = mount(keepAlive(RepoCommitSection), { store }); - } - - function setupDefaultState() { - store.state.noChangesStateSvgPath = 'svg'; - store.state.committedStateSvgPath = 'commitsvg'; - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'main'; - store.state.projects.abcproject = { - web_url: '', - branches: { - main: { - workingReference: '1', - }, - }, - }; - - const files = [file('file1'), file('file2')].map((f) => - Object.assign(f, { - type: 'blob', - content: 'orginal content', - }), - ); - - store.state.currentBranch = 'main'; - store.state.changedFiles = []; - store.state.stagedFiles = [{ ...files[0] }, { ...files[1] }]; - store.state.stagedFiles.forEach((f) => - Object.assign(f, { - changed: true, - staged: true, - content: 'testing', - }), - ); - - files.forEach((f) => { - store.state.entries[f.path] = f; - }); - } - - beforeEach(() => { - store = createStore(); - router = createRouter(store); - - jest.spyOn(store, 'dispatch'); - jest.spyOn(router, 'push').mockImplementation(); - }); - - describe('empty state', () => { - beforeEach(() => { - store.state.noChangesStateSvgPath = TEST_NO_CHANGES_SVG; - store.state.committedStateSvgPath = 'svg'; - - createComponent(); - }); - - it('renders empty state component', () => { - expect(wrapper.findComponent(EmptyState).exists()).toBe(true); - }); - }); - - describe('default', () => { - beforeEach(() => { - setupDefaultState(); - - createComponent(); - }); - - it('opens last opened file', () => { - expect(store.state.openFiles.length).toBe(1); - expect(store.state.openFiles[0].pending).toBe(true); - }); - - it('calls openPendingTab', () => { - expect(store.dispatch).toHaveBeenCalledWith('openPendingTab', { - file: store.getters.lastOpenedFile, - keyPrefix: stageKeys.staged, - }); - }); - - it('renders a commit section', () => { - const allFiles = store.state.changedFiles.concat(store.state.stagedFiles); - const changedFileNames = wrapper - .findAll('.multi-file-commit-list > li') - .wrappers.map((x) => x.text().trim()); - - expect(changedFileNames).toEqual(allFiles.map((x) => x.path)); - }); - - it('does not show empty state', () => { - expect(wrapper.findComponent(EmptyState).exists()).toBe(false); - }); - }); - - describe('if nothing is changed or staged', () => { - beforeEach(() => { - setupDefaultState(); - - store.state.openFiles = [...Object.values(store.state.entries)]; - store.state.openFiles[0].active = true; - store.state.stagedFiles = []; - - createComponent(); - }); - - it('opens currently active file', () => { - expect(store.state.openFiles.length).toBe(1); - expect(store.state.openFiles[0].pending).toBe(true); - - expect(store.dispatch).toHaveBeenCalledWith('openPendingTab', { - file: store.state.entries[store.getters.activeFile.path], - keyPrefix: stageKeys.unstaged, - }); - }); - }); - - describe('with unstaged file', () => { - beforeEach(() => { - setupDefaultState(); - - store.state.changedFiles = store.state.stagedFiles.map((x) => - Object.assign(x, { staged: false }), - ); - store.state.stagedFiles = []; - - createComponent(); - }); - - it('calls openPendingTab with unstaged prefix', () => { - expect(store.dispatch).toHaveBeenCalledWith('openPendingTab', { - file: store.getters.lastOpenedFile, - keyPrefix: stageKeys.unstaged, - }); - }); - - it('does not show empty state', () => { - expect(wrapper.findComponent(EmptyState).exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js deleted file mode 100644 index 5cf3eb9f8b7..00000000000 --- a/spec/frontend/ide/components/repo_editor_spec.js +++ /dev/null @@ -1,809 +0,0 @@ -import { GlTab } from '@gitlab/ui'; -import MockAdapter from 'axios-mock-adapter'; -import { editor as monacoEditor, Range } from 'monaco-editor'; -import { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { shallowMount } from '@vue/test-utils'; -import waitForPromises from 'helpers/wait_for_promises'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import { exampleConfigs, exampleFiles } from 'jest/ide/lib/editorconfig/mock_data'; -import { EDITOR_CODE_INSTANCE_FN, EDITOR_DIFF_INSTANCE_FN } from '~/editor/constants'; -import { EditorMarkdownExtension } from '~/editor/extensions/source_editor_markdown_ext'; -import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_editor_markdown_livepreview_ext'; -import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext'; -import SourceEditor from '~/editor/source_editor'; -import RepoEditor from '~/ide/components/repo_editor.vue'; -import { leftSidebarViews, FILE_VIEW_MODE_PREVIEW, viewerTypes } from '~/ide/constants'; -import { DEFAULT_CI_CONFIG_PATH } from '~/lib/utils/constants'; -import ModelManager from '~/ide/lib/common/model_manager'; -import service from '~/ide/services'; -import { createStoreOptions } from '~/ide/stores'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; -import SourceEditorInstance from '~/editor/source_editor_instance'; -import { file } from '../helpers'; - -jest.mock('~/behaviors/markdown/render_gfm'); -jest.mock('~/editor/extensions/source_editor_ci_schema_ext'); - -const PREVIEW_MARKDOWN_PATH = '/foo/bar/preview_markdown'; -const CURRENT_PROJECT_ID = 'gitlab-org/gitlab'; - -const dummyFile = { - text: { - ...file('file.txt'), - content: 'hello world', - active: true, - tempFile: true, - }, - markdown: { - ...file('sample.md'), - projectId: 'namespace/project', - path: 'sample.md', - content: 'hello world', - tempFile: true, - active: true, - }, - binary: { - ...file('file.dat'), - content: '🐱', // non-ascii binary content, - tempFile: true, - active: true, - }, - ciConfig: { - ...file(DEFAULT_CI_CONFIG_PATH), - content: '', - tempFile: true, - active: true, - }, - empty: { - ...file('empty'), - tempFile: false, - content: '', - raw: '', - }, -}; - -const createActiveFile = (props) => { - return { - ...dummyFile.text, - ...props, - }; -}; - -const prepareStore = (state, activeFile) => { - const localState = { - openFiles: [activeFile], - projects: { - [CURRENT_PROJECT_ID]: { - branches: { - main: { - name: 'main', - commit: { - id: 'abcdefgh', - }, - }, - }, - }, - }, - currentProjectId: CURRENT_PROJECT_ID, - currentBranchId: 'main', - entries: { - [activeFile.path]: activeFile, - }, - previewMarkdownPath: PREVIEW_MARKDOWN_PATH, - }; - const storeOptions = createStoreOptions(); - return new Vuex.Store({ - ...createStoreOptions(), - state: { - ...storeOptions.state, - ...localState, - ...state, - }, - }); -}; - -describe('RepoEditor', () => { - let wrapper; - let vm; - let createInstanceSpy; - let createDiffInstanceSpy; - let createModelSpy; - let applyExtensionSpy; - let removeExtensionSpy; - let extensionsStore; - let store; - - const waitForEditorSetup = () => - new Promise((resolve) => { - vm.$once('editorSetup', resolve); - }); - - const createComponent = async ({ state = {}, activeFile = dummyFile.text } = {}) => { - store = prepareStore(state, activeFile); - wrapper = shallowMount(RepoEditor, { - store, - propsData: { - file: store.state.openFiles[0], - }, - mocks: { - ContentViewer, - }, - }); - await waitForPromises(); - vm = wrapper.vm; - extensionsStore = wrapper.vm.globalEditor.extensionsStore; - jest.spyOn(vm, 'getFileData').mockResolvedValue(); - jest.spyOn(vm, 'getRawFileData').mockResolvedValue(); - }; - - const findEditor = () => wrapper.find('[data-testid="editor-container"]'); - const findTabs = () => wrapper.findAllComponents(GlTab); - const findPreviewTab = () => wrapper.find('[data-testid="preview-tab"]'); - - beforeEach(() => { - stubPerformanceWebAPI(); - - createInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_CODE_INSTANCE_FN); - createDiffInstanceSpy = jest.spyOn(SourceEditor.prototype, EDITOR_DIFF_INSTANCE_FN); - createModelSpy = jest.spyOn(monacoEditor, 'createModel'); - applyExtensionSpy = jest.spyOn(SourceEditorInstance.prototype, 'use'); - removeExtensionSpy = jest.spyOn(SourceEditorInstance.prototype, 'unuse'); - jest.spyOn(service, 'getFileData').mockResolvedValue(); - jest.spyOn(service, 'getRawFileData').mockResolvedValue(); - }); - - afterEach(() => { - // create a new model each time, otherwise tests conflict with each other - // because of same model being used in multiple tests - monacoEditor.getModels().forEach((model) => model.dispose()); - }); - - describe('default', () => { - it.each` - boolVal | textVal - ${true} | ${'all'} - ${false} | ${'none'} - `('sets renderWhitespace to "$textVal"', async ({ boolVal, textVal } = {}) => { - await createComponent({ - state: { - renderWhitespaceInCode: boolVal, - }, - }); - expect(vm.editorOptions.renderWhitespace).toEqual(textVal); - }); - - it('renders an ide container', async () => { - await createComponent(); - expect(findEditor().isVisible()).toBe(true); - }); - - it('renders no tabs', async () => { - await createComponent(); - const tabs = findTabs(); - - expect(tabs).toHaveLength(0); - }); - }); - - describe('schema registration for .gitlab-ci.yml', () => { - const setup = async (activeFile) => { - await createComponent(); - vm.editor.registerCiSchema = jest.fn(); - if (activeFile) { - wrapper.setProps({ file: activeFile }); - } - await waitForPromises(); - await nextTick(); - }; - it.each` - activeFile | shouldUseExtension | desc - ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`} - ${dummyFile.ciConfig} | ${true} | ${`file is CI config; should`} - `( - 'when the activeFile is "$activeFile", $desc use extension', - async ({ activeFile, shouldUseExtension }) => { - await setup(activeFile); - - if (shouldUseExtension) { - expect(applyExtensionSpy).toHaveBeenCalledWith({ - definition: CiSchemaExtension, - }); - } else { - expect(applyExtensionSpy).not.toHaveBeenCalledWith({ - definition: CiSchemaExtension, - }); - } - }, - ); - it('stores the fetched extension and does not double-fetch the schema', async () => { - await setup(); - expect(CiSchemaExtension).toHaveBeenCalledTimes(0); - - wrapper.setProps({ file: dummyFile.ciConfig }); - await waitForPromises(); - await nextTick(); - expect(CiSchemaExtension).toHaveBeenCalledTimes(1); - expect(vm.CiSchemaExtension).toEqual(CiSchemaExtension); - expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(1); - - wrapper.setProps({ file: dummyFile.markdown }); - await waitForPromises(); - await nextTick(); - expect(CiSchemaExtension).toHaveBeenCalledTimes(1); - expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(1); - - wrapper.setProps({ file: dummyFile.ciConfig }); - await waitForPromises(); - await nextTick(); - expect(CiSchemaExtension).toHaveBeenCalledTimes(1); - expect(vm.editor.registerCiSchema).toHaveBeenCalledTimes(2); - }); - it('unuses the existing CI extension if the new model is not CI config', async () => { - await setup(dummyFile.ciConfig); - - expect(removeExtensionSpy).not.toHaveBeenCalled(); - wrapper.setProps({ file: dummyFile.markdown }); - await waitForPromises(); - await nextTick(); - expect(removeExtensionSpy).toHaveBeenCalledWith(CiSchemaExtension); - }); - }); - - describe('when file is markdown', () => { - let mock; - let activeFile; - - beforeEach(() => { - activeFile = dummyFile.markdown; - - mock = new MockAdapter(axios); - - mock.onPost(/(.*)\/preview_markdown/).reply(HTTP_STATUS_OK, { - body: `

${dummyFile.text.content}

`, - }); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('when files is markdown', () => { - beforeEach(async () => { - await createComponent({ activeFile }); - }); - - it('renders an Edit and a Preview Tab', () => { - const tabs = findTabs(); - - expect(tabs).toHaveLength(2); - expect(tabs.at(0).element.dataset.testid).toBe('edit-tab'); - expect(tabs.at(1).element.dataset.testid).toBe('preview-tab'); - }); - - it('renders markdown for tempFile', async () => { - findPreviewTab().vm.$emit('click'); - await waitForPromises(); - expect(wrapper.findComponent(ContentViewer).html()).toContain(dummyFile.text.content); - }); - - describe('when file changes to non-markdown file', () => { - beforeEach(() => { - wrapper.setProps({ file: dummyFile.empty }); - }); - - it('should hide tabs', () => { - expect(findTabs()).toHaveLength(0); - }); - }); - }); - - it('when not in edit mode, shows no tabs', async () => { - await createComponent({ - state: { - currentActivityView: leftSidebarViews.review.name, - }, - activeFile, - }); - expect(findTabs()).toHaveLength(0); - }); - }); - - describe('when file is binary and not raw', () => { - beforeEach(async () => { - const activeFile = dummyFile.binary; - await createComponent({ activeFile }); - }); - - it('does not render the IDE', () => { - expect(findEditor().isVisible()).toBe(false); - }); - - it('does not create an instance', () => { - expect(createInstanceSpy).not.toHaveBeenCalled(); - expect(createDiffInstanceSpy).not.toHaveBeenCalled(); - }); - }); - - describe('createEditorInstance', () => { - it.each` - viewer | diffInstance - ${viewerTypes.edit} | ${undefined} - ${viewerTypes.diff} | ${true} - ${viewerTypes.mr} | ${true} - `( - 'creates instance of correct type when viewer is $viewer', - async ({ viewer, diffInstance }) => { - await createComponent({ - state: { viewer }, - }); - const isDiff = () => { - return diffInstance ? { isDiff: true } : {}; - }; - expect(createInstanceSpy).toHaveBeenCalledWith(expect.objectContaining(isDiff())); - expect(createDiffInstanceSpy).toHaveBeenCalledTimes((diffInstance && 1) || 0); - }, - ); - - it('installs the WebIDE extension', async () => { - await createComponent(); - expect(applyExtensionSpy).toHaveBeenCalled(); - const ideExtensionApi = extensionsStore.get('EditorWebIde').api; - Reflect.ownKeys(ideExtensionApi).forEach((fn) => { - expect(vm.editor[fn]).toBeDefined(); - expect(vm.editor.methods[fn]).toBe('EditorWebIde'); - }); - }); - }); - - describe('setupEditor', () => { - it('creates new model on load', async () => { - await createComponent(); - // We always create two models per file to be able to build a diff of changes - expect(createModelSpy).toHaveBeenCalledTimes(2); - // The model with the most recent changes is the last one - const [content] = createModelSpy.mock.calls[1]; - expect(content).toBe(dummyFile.text.content); - }); - - it('does not create a new model on subsequent calls to setupEditor and re-uses the already-existing model', async () => { - await createComponent(); - const existingModel = vm.model; - createModelSpy.mockClear(); - - vm.setupEditor(); - - expect(createModelSpy).not.toHaveBeenCalled(); - expect(vm.model).toBe(existingModel); - }); - - it('updates state with the value of the model', async () => { - await createComponent(); - const newContent = 'As Gregor Samsa\n awoke one morning\n'; - vm.model.setValue(newContent); - - vm.setupEditor(); - - expect(vm.file.content).toBe(newContent); - }); - - it('sets head model as staged file', async () => { - await createComponent(); - vm.modelManager.dispose(); - const addModelSpy = jest.spyOn(ModelManager.prototype, 'addModel'); - - vm.$store.state.stagedFiles.push({ ...vm.file, key: 'staged' }); - vm.file.staged = true; - vm.file.key = `unstaged-${vm.file.key}`; - - vm.setupEditor(); - - expect(addModelSpy).toHaveBeenCalledWith(vm.file, vm.$store.state.stagedFiles[0]); - }); - - it.each` - prefix | activeFile | viewer | shouldHaveMarkdownExtension - ${'Should not'} | ${dummyFile.text} | ${viewerTypes.edit} | ${false} - ${'Should'} | ${dummyFile.markdown} | ${viewerTypes.edit} | ${true} - ${'Should not'} | ${dummyFile.empty} | ${viewerTypes.edit} | ${false} - ${'Should not'} | ${dummyFile.text} | ${viewerTypes.diff} | ${false} - ${'Should not'} | ${dummyFile.markdown} | ${viewerTypes.diff} | ${false} - ${'Should not'} | ${dummyFile.empty} | ${viewerTypes.diff} | ${false} - ${'Should not'} | ${dummyFile.text} | ${viewerTypes.mr} | ${false} - ${'Should not'} | ${dummyFile.markdown} | ${viewerTypes.mr} | ${false} - ${'Should not'} | ${dummyFile.empty} | ${viewerTypes.mr} | ${false} - `( - '$prefix install markdown extension for $activeFile.name in $viewer viewer', - async ({ activeFile, viewer, shouldHaveMarkdownExtension } = {}) => { - await createComponent({ state: { viewer }, activeFile }); - - if (shouldHaveMarkdownExtension) { - expect(applyExtensionSpy).toHaveBeenCalledWith({ - definition: EditorMarkdownPreviewExtension, - setupOptions: { previewMarkdownPath: PREVIEW_MARKDOWN_PATH }, - }); - // TODO: spying on extensions causes Jest to blow up, so we have to assert on - // the public property the extension adds, as opposed to the args passed to the ctor - expect(wrapper.vm.editor.markdownPreview.path).toBe(PREVIEW_MARKDOWN_PATH); - } else { - expect(applyExtensionSpy).not.toHaveBeenCalledWith( - wrapper.vm.editor, - expect.any(EditorMarkdownExtension), - ); - } - }, - ); - - it('fetches the live preview extension even if markdown is not the first opened file', async () => { - const textFile = dummyFile.text; - const mdFile = dummyFile.markdown; - const previewExtConfig = { - definition: EditorMarkdownPreviewExtension, - setupOptions: { previewMarkdownPath: PREVIEW_MARKDOWN_PATH }, - }; - await createComponent({ activeFile: textFile }); - applyExtensionSpy.mockClear(); - - await wrapper.setProps({ file: mdFile }); - await waitForPromises(); - - expect(applyExtensionSpy).toHaveBeenCalledWith(previewExtConfig); - }); - }); - - describe('editor tabs', () => { - beforeEach(async () => { - await createComponent(); - }); - - it.each` - mode | isVisible - ${'edit'} | ${false} - ${'review'} | ${false} - ${'commit'} | ${false} - `('tabs in $mode are $isVisible', async ({ mode, isVisible } = {}) => { - vm.$store.state.currentActivityView = leftSidebarViews[mode].name; - - await nextTick(); - expect(wrapper.find('.nav-links').exists()).toBe(isVisible); - }); - }); - - describe('files in preview mode', () => { - const changeViewMode = (viewMode) => - vm.$store.dispatch('editor/updateFileEditor', { - path: vm.file.path, - data: { viewMode }, - }); - - beforeEach(async () => { - await createComponent({ - activeFile: dummyFile.markdown, - }); - - changeViewMode(FILE_VIEW_MODE_PREVIEW); - await nextTick(); - }); - - it('do not show the editor', () => { - expect(vm.showEditor).toBe(false); - expect(findEditor().isVisible()).toBe(false); - }); - }); - - describe('initEditor', () => { - const hideEditorAndRunFn = async () => { - jest.clearAllMocks(); - jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true); - - vm.initEditor(); - await nextTick(); - }; - - it('does not fetch file information for temp entries', async () => { - await createComponent({ - activeFile: dummyFile.text, - }); - - expect(vm.getFileData).not.toHaveBeenCalled(); - }); - - it('is being initialised for files without content even if shouldHideEditor is `true`', async () => { - await createComponent({ - activeFile: dummyFile.empty, - }); - - await hideEditorAndRunFn(); - - expect(vm.getFileData).toHaveBeenCalled(); - expect(vm.getRawFileData).toHaveBeenCalled(); - }); - - it('does not initialize editor for files already with content when shouldHideEditor is `true`', async () => { - await createComponent({ - activeFile: dummyFile.text, - }); - - await hideEditorAndRunFn(); - - expect(vm.getFileData).not.toHaveBeenCalled(); - expect(vm.getRawFileData).not.toHaveBeenCalled(); - expect(createInstanceSpy).not.toHaveBeenCalled(); - }); - }); - - describe('updates on file changes', () => { - beforeEach(async () => { - await createComponent({ - activeFile: createActiveFile({ - content: 'foo', // need to prevent full cycle of initEditor - }), - }); - jest.spyOn(vm, 'initEditor').mockImplementation(); - }); - - it('calls removePendingTab when old file is pending', async () => { - jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true); - jest.spyOn(vm, 'removePendingTab').mockImplementation(); - - const origFile = vm.file; - vm.file.pending = true; - await nextTick(); - - wrapper.setProps({ - file: file('testing'), - }); - vm.file.content = 'foo'; // need to prevent full cycle of initEditor - await nextTick(); - - expect(vm.removePendingTab).toHaveBeenCalledWith(origFile); - }); - - it('does not call initEditor if the file did not change', async () => { - const newFile = { ...store.state.openFiles[0] }; - wrapper.setProps({ - file: newFile, - }); - await nextTick(); - - expect(vm.initEditor).not.toHaveBeenCalled(); - }); - - it('calls initEditor when file key is changed', async () => { - expect(vm.initEditor).not.toHaveBeenCalled(); - - wrapper.setProps({ - file: { - ...vm.file, - key: 'new', - }, - }); - await nextTick(); - - expect(vm.initEditor).toHaveBeenCalled(); - }); - }); - - describe('populates editor with the fetched content', () => { - const createRemoteFile = (name) => ({ - ...file(name), - tmpFile: false, - }); - - beforeEach(async () => { - await createComponent(); - vm.getRawFileData.mockRestore(); - }); - - it('after switching viewer from edit to diff', async () => { - const f = createRemoteFile('newFile'); - store.state.entries[f.path] = f; - jest.spyOn(service, 'getRawFileData').mockImplementation(() => { - expect(vm.file.loading).toBe(true); - - // switching from edit to diff mode usually triggers editor initialization - vm.$store.state.viewer = viewerTypes.diff; - - jest.runOnlyPendingTimers(); - - return Promise.resolve('rawFileData123\n'); - }); - - wrapper.setProps({ - file: f, - }); - - await waitForEditorSetup(); - expect(vm.model.getModel().getValue()).toBe('rawFileData123\n'); - }); - - it('after opening multiple files at the same time', async () => { - const fileA = createRemoteFile('fileA'); - const aContent = 'fileA-rawContent\n'; - const bContent = 'fileB-rawContent\n'; - const fileB = createRemoteFile('fileB'); - store.state.entries[fileA.path] = fileA; - store.state.entries[fileB.path] = fileB; - - jest - .spyOn(service, 'getRawFileData') - .mockImplementation(() => { - // opening fileB while the content of fileA is still being fetched - wrapper.setProps({ - file: fileB, - }); - return Promise.resolve(aContent); - }) - .mockImplementationOnce(() => { - // we delay returning fileB content - // to make sure the editor doesn't initialize prematurely - jest.advanceTimersByTime(30); - return Promise.resolve(bContent); - }); - - wrapper.setProps({ - file: fileA, - }); - - await waitForEditorSetup(); - expect(vm.model.getModel().getValue()).toBe(bContent); - }); - }); - - describe('onPaste', () => { - const setFileName = (name) => - createActiveFile({ - content: 'hello world\n', - name, - path: `foo/${name}`, - key: 'new', - }); - - const pasteImage = () => { - window.dispatchEvent( - Object.assign(new Event('paste'), { - clipboardData: { - files: [new File(['foo'], 'foo.png', { type: 'image/png' })], - }, - }), - ); - }; - - const watchState = (watched) => - new Promise((resolve) => { - const unwatch = vm.$store.watch(watched, () => { - unwatch(); - resolve(); - }); - }); - - // Pasting an image does a lot of things like using the FileReader API, - // so, waitForPromises isn't very reliable (and causes a flaky spec) - // Read more about state.watch: https://vuex.vuejs.org/api/#watch - const waitForFileContentChange = () => watchState((s) => s.entries['foo/bar.md'].content); - - beforeEach(async () => { - await createComponent({ - state: { - trees: { - 'gitlab-org/gitlab': { tree: [] }, - }, - currentProjectId: 'gitlab-org', - currentBranchId: 'gitlab', - }, - activeFile: setFileName('bar.md'), - }); - - // set cursor to line 2, column 1 - vm.editor.setSelection(new Range(2, 1, 2, 1)); - vm.editor.focus(); - - jest.spyOn(vm.editor, 'hasTextFocus').mockReturnValue(true); - }); - - it('adds an image entry to the same folder for a pasted image in a markdown file', async () => { - pasteImage(); - - await waitForFileContentChange(); - expect(vm.$store.state.entries['foo/foo.png'].rawPath.startsWith('blob:')).toBe(true); - expect(vm.$store.state.entries['foo/foo.png']).toMatchObject({ - path: 'foo/foo.png', - type: 'blob', - content: 'foo', - rawPath: vm.$store.state.entries['foo/foo.png'].rawPath, - }); - }); - - it("adds a markdown image tag to the file's contents", async () => { - pasteImage(); - - await waitForFileContentChange(); - expect(vm.file.content).toBe('hello world\n![foo.png](./foo.png)'); - }); - - it("does not add file to state or set markdown image syntax if the file isn't markdown", async () => { - await wrapper.setProps({ - file: setFileName('myfile.txt'), - }); - pasteImage(); - - await waitForPromises(); - expect(vm.$store.state.entries['foo/foo.png']).toBeUndefined(); - expect(vm.file.content).toBe('hello world\n'); - }); - }); - - describe('fetchEditorconfigRules', () => { - it.each(exampleFiles)( - 'does not fetch content from remote for .editorconfig files present locally (case %#)', - async ({ path, monacoRules }) => { - await createComponent({ - state: { - entries: (() => { - const res = {}; - exampleConfigs.forEach(({ path: configPath, content }) => { - res[configPath] = { ...file(), path: configPath, content }; - }); - return res; - })(), - }, - activeFile: createActiveFile({ - path, - key: path, - name: 'myfile.txt', - content: 'hello world', - }), - }); - - expect(vm.rules).toEqual(monacoRules); - expect(vm.model.options).toMatchObject(monacoRules); - expect(vm.getFileData).not.toHaveBeenCalled(); - expect(vm.getRawFileData).not.toHaveBeenCalled(); - }, - ); - - it('fetches content from remote for .editorconfig files not available locally', async () => { - const activeFile = createActiveFile({ - path: 'foo/bar/baz/test/my_spec.js', - key: 'foo/bar/baz/test/my_spec.js', - name: 'myfile.txt', - content: 'hello world', - }); - - const expectations = [ - 'foo/bar/baz/.editorconfig', - 'foo/bar/.editorconfig', - 'foo/.editorconfig', - '.editorconfig', - ]; - - await createComponent({ - state: { - entries: (() => { - const res = { - [activeFile.path]: activeFile, - }; - exampleConfigs.forEach(({ path: configPath }) => { - const f = { ...file(), path: configPath }; - delete f.content; - delete f.raw; - res[configPath] = f; - }); - return res; - })(), - }, - activeFile, - }); - - expect(service.getFileData.mock.calls.map(([args]) => args)).toEqual( - expectations.map((expectation) => expect.stringContaining(expectation)), - ); - expect(service.getRawFileData.mock.calls.map(([args]) => args)).toEqual( - expectations.map((expectation) => expect.objectContaining({ path: expectation })), - ); - }); - }); -}); diff --git a/spec/frontend/ide/components/repo_tab_spec.js b/spec/frontend/ide/components/repo_tab_spec.js deleted file mode 100644 index 08e8062a45a..00000000000 --- a/spec/frontend/ide/components/repo_tab_spec.js +++ /dev/null @@ -1,184 +0,0 @@ -import { GlTab } from '@gitlab/ui'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { stubComponent } from 'helpers/stub_component'; -import RepoTab from '~/ide/components/repo_tab.vue'; -import { createStore } from '~/ide/stores'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { file } from '../helpers'; - -Vue.use(Vuex); - -const GlTabStub = stubComponent(GlTab, { - template: '
  • ', -}); - -describe('RepoTab', () => { - let wrapper; - let store; - - const pushMock = jest.fn(); - const findTab = () => wrapper.findComponent(GlTabStub); - const findCloseButton = () => wrapper.findByTestId('close-button'); - - function createComponent(propsData) { - wrapper = mountExtended(RepoTab, { - store, - propsData, - stubs: { - GlTab: GlTabStub, - }, - mocks: { - $router: { - push: pushMock, - }, - }, - }); - } - - beforeEach(() => { - store = createStore(); - }); - - it('renders a close link and a name link', () => { - const tab = file(); - createComponent({ - tab, - }); - store.state.openFiles.push(tab); - const name = wrapper.find(`[title]`); - - expect(findCloseButton().html()).toContain('#close'); - expect(name.text()).toBe(tab.name); - }); - - it('does not call openPendingTab when tab is active', async () => { - createComponent({ - tab: { - ...file(), - pending: true, - active: true, - }, - }); - - jest.spyOn(store, 'dispatch'); - - await findTab().vm.$emit('click'); - - expect(store.dispatch).not.toHaveBeenCalledWith('openPendingTab'); - }); - - it('fires clickFile when the link is clicked', async () => { - const { getters } = store; - const tab = file(); - createComponent({ tab }); - - await findTab().vm.$emit('click', tab); - - expect(pushMock).toHaveBeenCalledWith(getters.getUrlForPath(tab.path)); - }); - - it('calls closeFile when clicking close button', async () => { - const tab = file(); - createComponent({ tab }); - store.state.entries[tab.path] = tab; - - jest.spyOn(store, 'dispatch'); - - await findCloseButton().trigger('click'); - - expect(store.dispatch).toHaveBeenCalledWith('closeFile', tab); - }); - - it('changes icon on hover', async () => { - const tab = file(); - tab.changed = true; - createComponent({ - tab, - }); - - await findTab().vm.$emit('mouseover'); - - expect(wrapper.find('.file-modified').exists()).toBe(false); - - await findTab().vm.$emit('mouseout'); - - expect(wrapper.find('.file-modified').exists()).toBe(true); - }); - - it.each` - tabProps | closeLabel - ${{}} | ${'Close foo.txt'} - ${{ changed: true }} | ${'foo.txt changed'} - `('close button has label ($closeLabel) with tab ($tabProps)', ({ tabProps, closeLabel }) => { - const tab = { ...file('foo.txt'), ...tabProps }; - - createComponent({ tab }); - - expect(findCloseButton().attributes('aria-label')).toBe(closeLabel); - }); - - describe('locked file', () => { - let f; - - beforeEach(() => { - f = file('locked file'); - f.file_lock = { - user: { - name: 'testuser', - updated_at: new Date(), - }, - }; - - createComponent({ - tab: f, - }); - }); - - it('renders lock icon', () => { - expect(wrapper.find('.file-status-icon')).not.toBeNull(); - }); - - it('renders a tooltip', () => { - expect(wrapper.find('span:nth-child(2)').attributes('title')).toBe('Locked by testuser'); - }); - }); - - describe('methods', () => { - describe('closeTab', () => { - it('closes tab if file has changed', async () => { - const tab = file(); - tab.changed = true; - tab.opened = true; - createComponent({ - tab, - }); - store.state.openFiles.push(tab); - store.state.changedFiles.push(tab); - store.state.entries[tab.path] = tab; - store.dispatch('setFileActive', tab.path); - - await findCloseButton().trigger('click'); - - expect(tab.opened).toBe(false); - expect(store.state.changedFiles).toHaveLength(1); - }); - - it('closes tab when clicking close btn', async () => { - const tab = file('lose'); - tab.opened = true; - createComponent({ - tab, - }); - store.state.openFiles.push(tab); - store.state.entries[tab.path] = tab; - store.dispatch('setFileActive', tab.path); - - await findCloseButton().trigger('click'); - - expect(tab.opened).toBe(false); - }); - }); - }); -}); diff --git a/spec/frontend/ide/components/repo_tabs_spec.js b/spec/frontend/ide/components/repo_tabs_spec.js deleted file mode 100644 index 9ced4babf1d..00000000000 --- a/spec/frontend/ide/components/repo_tabs_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import { mount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import RepoTabs from '~/ide/components/repo_tabs.vue'; -import { createStore } from '~/ide/stores'; -import { file } from '../helpers'; - -Vue.use(Vuex); - -describe('RepoTabs', () => { - let wrapper; - let store; - - beforeEach(() => { - store = createStore(); - store.state.openFiles = [file('open1'), file('open2')]; - - wrapper = mount(RepoTabs, { - propsData: { - files: store.state.openFiles, - viewer: 'editor', - activeFile: file('activeFile'), - }, - store, - }); - }); - - it('renders a list of tabs', async () => { - store.state.openFiles[0].active = true; - - await nextTick(); - const tabs = [...wrapper.vm.$el.querySelectorAll('.multi-file-tab')]; - - expect(tabs.length).toEqual(2); - expect(tabs[0].parentNode.classList.contains('active')).toEqual(true); - expect(tabs[1].parentNode.classList.contains('active')).toEqual(false); - }); -}); diff --git a/spec/frontend/ide/components/resizable_panel_spec.js b/spec/frontend/ide/components/resizable_panel_spec.js deleted file mode 100644 index 0529ea1918d..00000000000 --- a/spec/frontend/ide/components/resizable_panel_spec.js +++ /dev/null @@ -1,108 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import ResizablePanel from '~/ide/components/resizable_panel.vue'; -import { SIDE_LEFT, SIDE_RIGHT } from '~/ide/constants'; -import PanelResizer from '~/vue_shared/components/panel_resizer.vue'; - -const TEST_WIDTH = 500; -const TEST_MIN_WIDTH = 400; - -describe('~/ide/components/resizable_panel', () => { - Vue.use(Vuex); - - let wrapper; - let store; - - beforeEach(() => { - store = new Vuex.Store({}); - jest.spyOn(store, 'dispatch').mockImplementation(); - }); - - const createComponent = (props = {}) => { - wrapper = shallowMount(ResizablePanel, { - propsData: { - initialWidth: TEST_WIDTH, - minSize: TEST_MIN_WIDTH, - side: SIDE_LEFT, - ...props, - }, - store, - }); - }; - const findResizer = () => wrapper.findComponent(PanelResizer); - const findInlineStyle = () => wrapper.element.style.cssText; - const createInlineStyle = (width) => `width: ${width}px;`; - - describe.each` - props | showResizer | resizerSide | expectedStyle - ${{ resizable: true, side: SIDE_LEFT }} | ${true} | ${SIDE_RIGHT} | ${createInlineStyle(TEST_WIDTH)} - ${{ resizable: true, side: SIDE_RIGHT }} | ${true} | ${SIDE_LEFT} | ${createInlineStyle(TEST_WIDTH)} - ${{ resizable: false, side: SIDE_LEFT }} | ${false} | ${SIDE_RIGHT} | ${''} - `('with props $props', ({ props, showResizer, resizerSide, expectedStyle }) => { - beforeEach(() => { - createComponent(props); - }); - - it(`show resizer is ${showResizer}`, () => { - const expectedDisplay = showResizer ? '' : 'none'; - const resizer = findResizer(); - - expect(resizer.exists()).toBe(true); - expect(resizer.element.style.display).toBe(expectedDisplay); - }); - - it(`resizer side is '${resizerSide}'`, () => { - const resizer = findResizer(); - - expect(resizer.props('side')).toBe(resizerSide); - }); - - it(`has style '${expectedStyle}'`, () => { - expect(findInlineStyle()).toBe(expectedStyle); - }); - }); - - describe('default', () => { - beforeEach(() => { - createComponent(); - }); - - it('does not dispatch anything', () => { - expect(store.dispatch).not.toHaveBeenCalled(); - }); - - it.each` - event | dispatchArgs - ${'resize-start'} | ${['setResizingStatus', true]} - ${'resize-end'} | ${['setResizingStatus', false]} - `('when resizer emits $event, dispatch $dispatchArgs', ({ event, dispatchArgs }) => { - const resizer = findResizer(); - - resizer.vm.$emit(event); - - expect(store.dispatch).toHaveBeenCalledWith(...dispatchArgs); - }); - - it('renders resizer', () => { - const resizer = findResizer(); - - expect(resizer.props()).toMatchObject({ - maxSize: window.innerWidth / 2, - minSize: TEST_MIN_WIDTH, - startSize: TEST_WIDTH, - }); - }); - - it('when resizer emits update:size, changes inline width', async () => { - const newSize = TEST_WIDTH - 100; - const resizer = findResizer(); - - resizer.vm.$emit('update:size', newSize); - - await nextTick(); - expect(findInlineStyle()).toBe(createInlineStyle(newSize)); - }); - }); -}); diff --git a/spec/frontend/ide/components/shared/tokened_input_spec.js b/spec/frontend/ide/components/shared/tokened_input_spec.js deleted file mode 100644 index 4bd5a6527e2..00000000000 --- a/spec/frontend/ide/components/shared/tokened_input_spec.js +++ /dev/null @@ -1,110 +0,0 @@ -import { mount } from '@vue/test-utils'; -import TokenedInput from '~/ide/components/shared/tokened_input.vue'; - -const TEST_PLACEHOLDER = 'Searching in test'; -const TEST_TOKENS = [ - { label: 'lorem', id: 1 }, - { label: 'ipsum', id: 2 }, - { label: 'dolar', id: 3 }, -]; -const TEST_VALUE = 'lorem'; - -function getTokenElements(wrapper) { - return wrapper.findAll('.filtered-search-token button'); -} - -describe('IDE shared/TokenedInput', () => { - let wrapper; - - const createComponent = (props = {}) => { - wrapper = mount(TokenedInput, { - propsData: { - tokens: TEST_TOKENS, - placeholder: TEST_PLACEHOLDER, - value: TEST_VALUE, - ...props, - }, - attachTo: document.body, - }); - }; - - it('renders tokens', () => { - createComponent(); - const renderedTokens = getTokenElements(wrapper).wrappers.map((w) => w.text()); - - expect(renderedTokens).toEqual(TEST_TOKENS.map((x) => x.label)); - }); - - it('renders input', () => { - createComponent(); - - expect(wrapper.find('input').element).toBeInstanceOf(HTMLInputElement); - expect(wrapper.find('input').element).toHaveValue(TEST_VALUE); - }); - - it('renders placeholder, when tokens are empty', () => { - createComponent({ tokens: [] }); - - expect(wrapper.find('input').attributes('placeholder')).toBe(TEST_PLACEHOLDER); - }); - - it('triggers "removeToken" on token click', async () => { - createComponent(); - await getTokenElements(wrapper).at(0).trigger('click'); - - expect(wrapper.emitted('removeToken')[0]).toStrictEqual([TEST_TOKENS[0]]); - }); - - it('removes token on backspace when value is empty', async () => { - createComponent({ value: '' }); - - expect(wrapper.emitted('removeToken')).toBeUndefined(); - - await wrapper.find('input').trigger('keyup.delete'); - await wrapper.find('input').trigger('keyup.delete'); - - expect(wrapper.emitted('removeToken')[0]).toStrictEqual([TEST_TOKENS[TEST_TOKENS.length - 1]]); - }); - - it('does not trigger "removeToken" on backspaces when value is not empty', async () => { - createComponent({ value: 'SOMETHING' }); - - await wrapper.find('input').trigger('keyup.delete'); - await wrapper.find('input').trigger('keyup.delete'); - - expect(wrapper.emitted('removeToken')).toBeUndefined(); - }); - - it('does not trigger "removeToken" on backspaces when tokens are empty', async () => { - createComponent({ value: '', tokens: [] }); - - await wrapper.find('input').trigger('keyup.delete'); - await wrapper.find('input').trigger('keyup.delete'); - - expect(wrapper.emitted('removeToken')).toBeUndefined(); - }); - - it('triggers "focus" on input focus', async () => { - createComponent(); - - await wrapper.find('input').trigger('focus'); - - expect(wrapper.emitted('focus')).toHaveLength(1); - }); - - it('triggers "blur" on input blur', async () => { - createComponent(); - - await wrapper.find('input').trigger('blur'); - - expect(wrapper.emitted('blur')).toHaveLength(1); - }); - - it('triggers "input" with value on input change', async () => { - createComponent(); - - await wrapper.find('input').setValue('something-else'); - - expect(wrapper.emitted('input')[0]).toStrictEqual(['something-else']); - }); -}); diff --git a/spec/frontend/ide/components/terminal/empty_state_spec.js b/spec/frontend/ide/components/terminal/empty_state_spec.js deleted file mode 100644 index 1f6337e87ee..00000000000 --- a/spec/frontend/ide/components/terminal/empty_state_spec.js +++ /dev/null @@ -1,102 +0,0 @@ -import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { TEST_HOST } from 'spec/test_constants'; -import TerminalEmptyState from '~/ide/components/terminal/empty_state.vue'; - -const TEST_HELP_PATH = `${TEST_HOST}/help/test`; -const TEST_PATH = `${TEST_HOST}/home.png`; -const TEST_HTML_MESSAGE = 'lorem ipsum'; - -describe('IDE TerminalEmptyState', () => { - let wrapper; - - const factory = (options = {}) => { - wrapper = shallowMount(TerminalEmptyState, { - ...options, - }); - }; - - it('does not show illustration, if no path specified', () => { - factory(); - - expect(wrapper.find('.svg-content').exists()).toBe(false); - }); - - it('shows illustration with path', () => { - factory({ - propsData: { - illustrationPath: TEST_PATH, - }, - }); - - const img = wrapper.find('.svg-content img'); - - expect(img.exists()).toBe(true); - expect(img.element.src).toBe(TEST_PATH); - }); - - it('when loading, shows loading icon', () => { - factory({ - propsData: { - isLoading: true, - }, - }); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - - it('when not loading, does not show loading icon', () => { - factory({ - propsData: { - isLoading: false, - }, - }); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); - }); - - describe('when valid', () => { - let button; - - beforeEach(() => { - factory({ - propsData: { - isLoading: false, - isValid: true, - helpPath: TEST_HELP_PATH, - }, - }); - - button = wrapper.findComponent(GlButton); - }); - - it('shows button', () => { - expect(button.text()).toBe('Start Web Terminal'); - expect(button.props('disabled')).toBe(false); - }); - - it('emits start when button is clicked', () => { - expect(wrapper.emitted().start).toBeUndefined(); - button.vm.$emit('click'); - - expect(wrapper.emitted().start).toHaveLength(1); - }); - - it('shows help path link', () => { - expect(wrapper.find('a').attributes('href')).toBe(TEST_HELP_PATH); - }); - }); - - it('when not valid, shows disabled button and message', () => { - factory({ - propsData: { - isLoading: false, - isValid: false, - message: TEST_HTML_MESSAGE, - }, - }); - - expect(wrapper.findComponent(GlButton).props('disabled')).toBe(true); - expect(wrapper.findComponent(GlAlert).html()).toContain(TEST_HTML_MESSAGE); - }); -}); diff --git a/spec/frontend/ide/components/terminal/session_spec.js b/spec/frontend/ide/components/terminal/session_spec.js deleted file mode 100644 index 54a9ad89f5c..00000000000 --- a/spec/frontend/ide/components/terminal/session_spec.js +++ /dev/null @@ -1,97 +0,0 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import TerminalSession from '~/ide/components/terminal/session.vue'; -import Terminal from '~/ide/components/terminal/terminal.vue'; -import { - STARTING, - PENDING, - RUNNING, - STOPPING, - STOPPED, -} from '~/ide/stores/modules/terminal/constants'; - -const TEST_TERMINAL_PATH = 'terminal/path'; - -Vue.use(Vuex); - -describe('IDE TerminalSession', () => { - let wrapper; - let actions; - let state; - - const factory = (options = {}) => { - const store = new Vuex.Store({ - modules: { - terminal: { - namespaced: true, - actions, - state, - }, - }, - }); - - wrapper = shallowMount(TerminalSession, { - store, - ...options, - }); - }; - - const findButton = () => wrapper.findComponent(GlButton); - - beforeEach(() => { - state = { - session: { status: RUNNING, terminalPath: TEST_TERMINAL_PATH }, - }; - actions = { - restartSession: jest.fn(), - stopSession: jest.fn(), - }; - }); - - it('is empty if session is falsey', () => { - state.session = null; - factory(); - - expect(wrapper.find('*').exists()).toBe(false); - }); - - it('shows terminal', () => { - factory(); - - expect(wrapper.findComponent(Terminal).props()).toEqual({ - terminalPath: TEST_TERMINAL_PATH, - status: RUNNING, - }); - }); - - [STARTING, PENDING, RUNNING].forEach((status) => { - it(`show stop button when status is ${status}`, async () => { - state.session = { status }; - factory(); - - const button = findButton(); - button.vm.$emit('click'); - - await nextTick(); - expect(button.text()).toEqual('Stop Terminal'); - expect(actions.stopSession).toHaveBeenCalled(); - }); - }); - - [STOPPING, STOPPED].forEach((status) => { - it(`show stop button when status is ${status}`, async () => { - state.session = { status }; - factory(); - - const button = findButton(); - button.vm.$emit('click'); - - await nextTick(); - expect(button.text()).toEqual('Restart Terminal'); - expect(actions.restartSession).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/ide/components/terminal/terminal_controls_spec.js b/spec/frontend/ide/components/terminal/terminal_controls_spec.js deleted file mode 100644 index c18934f0f3b..00000000000 --- a/spec/frontend/ide/components/terminal/terminal_controls_spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { nextTick } from 'vue'; -import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue'; -import TerminalControls from '~/ide/components/terminal/terminal_controls.vue'; - -describe('IDE TerminalControls', () => { - let wrapper; - let buttons; - - const factory = (options = {}) => { - wrapper = shallowMount(TerminalControls, { - ...options, - }); - - buttons = wrapper.findAllComponents(ScrollButton); - }; - - it('shows an up and down scroll button', () => { - factory(); - - expect(buttons.wrappers.map((x) => x.props())).toEqual([ - expect.objectContaining({ direction: 'up', disabled: true }), - expect.objectContaining({ direction: 'down', disabled: true }), - ]); - }); - - it('enables up button with prop', () => { - factory({ propsData: { canScrollUp: true } }); - - expect(buttons.at(0).props()).toEqual( - expect.objectContaining({ direction: 'up', disabled: false }), - ); - }); - - it('enables down button with prop', () => { - factory({ propsData: { canScrollDown: true } }); - - expect(buttons.at(1).props()).toEqual( - expect.objectContaining({ direction: 'down', disabled: false }), - ); - }); - - it('emits "scroll-up" when click up button', async () => { - factory({ propsData: { canScrollUp: true } }); - - expect(wrapper.emitted()).toEqual({}); - - buttons.at(0).vm.$emit('click'); - - await nextTick(); - expect(wrapper.emitted('scroll-up')).toEqual([[]]); - }); - - it('emits "scroll-down" when click down button', async () => { - factory({ propsData: { canScrollDown: true } }); - - expect(wrapper.emitted()).toEqual({}); - - buttons.at(1).vm.$emit('click'); - - await nextTick(); - expect(wrapper.emitted('scroll-down')).toEqual([[]]); - }); -}); diff --git a/spec/frontend/ide/components/terminal/terminal_spec.js b/spec/frontend/ide/components/terminal/terminal_spec.js deleted file mode 100644 index f217ce6e0ef..00000000000 --- a/spec/frontend/ide/components/terminal/terminal_spec.js +++ /dev/null @@ -1,219 +0,0 @@ -import { GlLoadingIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue, { nextTick } from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import Terminal from '~/ide/components/terminal/terminal.vue'; -import TerminalControls from '~/ide/components/terminal/terminal_controls.vue'; -import { - STARTING, - PENDING, - RUNNING, - STOPPING, - STOPPED, -} from '~/ide/stores/modules/terminal/constants'; -import GLTerminal from '~/terminal/terminal'; - -const TEST_TERMINAL_PATH = 'terminal/path'; - -Vue.use(Vuex); - -jest.mock('~/terminal/terminal', () => - jest.fn().mockImplementation(function FakeTerminal() { - Object.assign(this, { - dispose: jest.fn(), - disable: jest.fn(), - addScrollListener: jest.fn(), - scrollToTop: jest.fn(), - scrollToBottom: jest.fn(), - }); - }), -); - -describe('IDE Terminal', () => { - let wrapper; - let state; - - const factory = (propsData) => { - const store = new Vuex.Store({ - state, - mutations: { - set(prevState, newState) { - Object.assign(prevState, newState); - }, - }, - }); - - wrapper = shallowMount(Terminal, { - propsData: { - status: RUNNING, - terminalPath: TEST_TERMINAL_PATH, - ...propsData, - }, - store, - }); - }; - - beforeEach(() => { - state = { - panelResizing: false, - }; - }); - - describe('loading text', () => { - [STARTING, PENDING].forEach((status) => { - it(`shows when starting (${status})`, () => { - factory({ status }); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - expect(wrapper.find('.top-bar').text()).toBe('Starting...'); - }); - }); - - it(`shows when stopping`, () => { - factory({ status: STOPPING }); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - expect(wrapper.find('.top-bar').text()).toBe('Stopping...'); - }); - - [RUNNING, STOPPED].forEach((status) => { - it('hides when not loading', () => { - factory({ status }); - - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); - expect(wrapper.find('.top-bar').text()).toBe(''); - }); - }); - }); - - describe('refs.terminal', () => { - it('has terminal path in data', () => { - factory(); - - expect(wrapper.vm.$refs.terminal.dataset.projectPath).toBe(TEST_TERMINAL_PATH); - }); - }); - - describe('terminal controls', () => { - beforeEach(() => { - factory(); - wrapper.vm.createTerminal(); - - return nextTick(); - }); - - it('is visible if terminal is created', () => { - expect(wrapper.findComponent(TerminalControls).exists()).toBe(true); - }); - - it('scrolls glterminal on scroll-up', () => { - wrapper.findComponent(TerminalControls).vm.$emit('scroll-up'); - - expect(wrapper.vm.glterminal.scrollToTop).toHaveBeenCalled(); - }); - - it('scrolls glterminal on scroll-down', () => { - wrapper.findComponent(TerminalControls).vm.$emit('scroll-down'); - - expect(wrapper.vm.glterminal.scrollToBottom).toHaveBeenCalled(); - }); - - it('has props set', () => { - expect(wrapper.findComponent(TerminalControls).props()).toEqual({ - canScrollUp: false, - canScrollDown: false, - }); - - const terminalInstance = GLTerminal.mock.instances[0]; - const scrollHandler = terminalInstance.addScrollListener.mock.calls[0][0]; - scrollHandler({ canScrollUp: true, canScrollDown: true }); - - return nextTick().then(() => { - expect(wrapper.findComponent(TerminalControls).props()).toEqual({ - canScrollUp: true, - canScrollDown: true, - }); - }); - }); - }); - - describe('refresh', () => { - it('creates the terminal if running', () => { - factory({ status: RUNNING, terminalPath: TEST_TERMINAL_PATH }); - - wrapper.vm.refresh(); - - expect(GLTerminal.mock.instances).toHaveLength(1); - }); - - it('stops the terminal if stopping', async () => { - factory({ status: RUNNING, terminalPath: TEST_TERMINAL_PATH }); - - wrapper.vm.refresh(); - - const terminal = GLTerminal.mock.instances[0]; - wrapper.setProps({ status: STOPPING }); - await nextTick(); - - expect(terminal.disable).toHaveBeenCalled(); - }); - }); - - describe('createTerminal', () => { - beforeEach(() => { - factory(); - wrapper.vm.createTerminal(); - }); - - it('creates the terminal', () => { - expect(GLTerminal).toHaveBeenCalledWith(wrapper.vm.$refs.terminal); - expect(wrapper.vm.glterminal).toBeInstanceOf(GLTerminal); - }); - - describe('scroll listener', () => { - it('has been called', () => { - expect(wrapper.vm.glterminal.addScrollListener).toHaveBeenCalled(); - }); - - it('updates scroll data when called', () => { - expect(wrapper.vm.canScrollUp).toBe(false); - expect(wrapper.vm.canScrollDown).toBe(false); - - const listener = wrapper.vm.glterminal.addScrollListener.mock.calls[0][0]; - listener({ canScrollUp: true, canScrollDown: true }); - - expect(wrapper.vm.canScrollUp).toBe(true); - expect(wrapper.vm.canScrollDown).toBe(true); - }); - }); - }); - - describe('destroyTerminal', () => { - it('calls dispose', () => { - factory(); - wrapper.vm.createTerminal(); - const disposeSpy = wrapper.vm.glterminal.dispose; - - expect(disposeSpy).not.toHaveBeenCalled(); - - wrapper.vm.destroyTerminal(); - - expect(disposeSpy).toHaveBeenCalled(); - expect(wrapper.vm.glterminal).toBe(null); - }); - }); - - describe('stopTerminal', () => { - it('calls disable', () => { - factory(); - wrapper.vm.createTerminal(); - - expect(wrapper.vm.glterminal.disable).not.toHaveBeenCalled(); - - wrapper.vm.stopTerminal(); - - expect(wrapper.vm.glterminal.disable).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/ide/components/terminal/view_spec.js b/spec/frontend/ide/components/terminal/view_spec.js deleted file mode 100644 index 2db3a163e3d..00000000000 --- a/spec/frontend/ide/components/terminal/view_spec.js +++ /dev/null @@ -1,92 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import waitForPromises from 'helpers/wait_for_promises'; -import { TEST_HOST } from 'spec/test_constants'; -import TerminalEmptyState from '~/ide/components/terminal/empty_state.vue'; -import TerminalSession from '~/ide/components/terminal/session.vue'; -import TerminalView from '~/ide/components/terminal/view.vue'; - -const TEST_HELP_PATH = `${TEST_HOST}/help`; -const TEST_SVG_PATH = `${TEST_HOST}/illustration.svg`; - -Vue.use(Vuex); - -describe('IDE TerminalView', () => { - let state; - let actions; - let getters; - let wrapper; - - const factory = async () => { - const store = new Vuex.Store({ - modules: { - terminal: { - namespaced: true, - state, - actions, - getters, - }, - }, - }); - - wrapper = shallowMount(TerminalView, { store }); - - // Uses deferred components, so wait for those to load... - await waitForPromises(); - }; - - beforeEach(() => { - state = { - isShowSplash: true, - paths: { - webTerminalHelpPath: TEST_HELP_PATH, - webTerminalSvgPath: TEST_SVG_PATH, - }, - }; - - actions = { - hideSplash: jest.fn().mockName('hideSplash'), - startSession: jest.fn().mockName('startSession'), - }; - - getters = { - allCheck: () => ({ - isLoading: false, - isValid: false, - message: 'bad', - }), - }; - }); - - it('renders empty state', async () => { - await factory(); - - expect(wrapper.findComponent(TerminalEmptyState).props()).toEqual({ - helpPath: TEST_HELP_PATH, - illustrationPath: TEST_SVG_PATH, - ...getters.allCheck(), - }); - }); - - it('hides splash and starts, when started', async () => { - await factory(); - - expect(actions.startSession).not.toHaveBeenCalled(); - expect(actions.hideSplash).not.toHaveBeenCalled(); - - wrapper.findComponent(TerminalEmptyState).vm.$emit('start'); - - expect(actions.startSession).toHaveBeenCalled(); - expect(actions.hideSplash).toHaveBeenCalled(); - }); - - it('shows Web Terminal when started', async () => { - state.isShowSplash = false; - await factory(); - - expect(wrapper.findComponent(TerminalEmptyState).exists()).toBe(false); - expect(wrapper.findComponent(TerminalSession).exists()).toBe(true); - }); -}); diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js deleted file mode 100644 index c5ec64ba6b2..00000000000 --- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_safe_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import TerminalSyncStatus from '~/ide/components/terminal_sync/terminal_sync_status.vue'; -import TerminalSyncStatusSafe from '~/ide/components/terminal_sync/terminal_sync_status_safe.vue'; - -Vue.use(Vuex); - -describe('ide/components/terminal_sync/terminal_sync_status_safe', () => { - let store; - let wrapper; - - const createComponent = () => { - store = new Vuex.Store({ - state: {}, - }); - - wrapper = shallowMount(TerminalSyncStatusSafe, { - store, - }); - }; - - beforeEach(createComponent); - - describe('with terminal sync module in store', () => { - beforeEach(() => { - store.registerModule('terminalSync', { - state: {}, - }); - }); - - it('renders terminal sync status', () => { - expect(wrapper.findComponent(TerminalSyncStatus).exists()).toBe(true); - }); - }); - - describe('without terminal sync module', () => { - it('does not render terminal sync status', () => { - expect(wrapper.findComponent(TerminalSyncStatus).exists()).toBe(false); - }); - }); -}); diff --git a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js b/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js deleted file mode 100644 index 47f06a10896..00000000000 --- a/spec/frontend/ide/components/terminal_sync/terminal_sync_status_spec.js +++ /dev/null @@ -1,94 +0,0 @@ -import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import TerminalSyncStatus from '~/ide/components/terminal_sync/terminal_sync_status.vue'; -import { - MSG_TERMINAL_SYNC_CONNECTING, - MSG_TERMINAL_SYNC_UPLOADING, - MSG_TERMINAL_SYNC_RUNNING, -} from '~/ide/stores/modules/terminal_sync/messages'; - -const TEST_MESSAGE = 'lorem ipsum dolar sit'; -const START_LOADING = 'START_LOADING'; - -Vue.use(Vuex); - -describe('ide/components/terminal_sync/terminal_sync_status', () => { - let moduleState; - let store; - let wrapper; - - const createComponent = () => { - store = new Vuex.Store({ - modules: { - terminalSync: { - namespaced: true, - state: moduleState, - mutations: { - [START_LOADING]: (state) => { - state.isLoading = true; - }, - }, - }, - }, - }); - - wrapper = shallowMount(TerminalSyncStatus, { - store, - }); - }; - - beforeEach(() => { - moduleState = { - isLoading: false, - isStarted: false, - isError: false, - message: '', - }; - }); - - describe('when doing nothing', () => { - it('shows nothing', () => { - createComponent(); - - expect(wrapper.find('*').exists()).toBe(false); - }); - }); - - describe.each` - description | state | statusMessage | icon - ${'when loading'} | ${{ isLoading: true }} | ${MSG_TERMINAL_SYNC_CONNECTING} | ${''} - ${'when loading and started'} | ${{ isLoading: true, isStarted: true }} | ${MSG_TERMINAL_SYNC_UPLOADING} | ${''} - ${'when error'} | ${{ isError: true, message: TEST_MESSAGE }} | ${TEST_MESSAGE} | ${'warning'} - ${'when started'} | ${{ isStarted: true }} | ${MSG_TERMINAL_SYNC_RUNNING} | ${'mobile-issue-close'} - `('$description', ({ state, statusMessage, icon }) => { - beforeEach(() => { - Object.assign(moduleState, state); - createComponent(); - }); - - it('shows message', () => { - expect(wrapper.attributes('title')).toContain(statusMessage); - }); - - if (!icon) { - it('does not render icon', () => { - expect(wrapper.findComponent(GlIcon).exists()).toBe(false); - }); - - it('renders loading icon', () => { - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - } else { - it('renders icon', () => { - expect(wrapper.findComponent(GlIcon).props('name')).toEqual(icon); - }); - - it('does not render loading icon', () => { - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); - }); - } - }); -}); diff --git a/spec/frontend/ide/file_helpers.js b/spec/frontend/ide/file_helpers.js deleted file mode 100644 index ace18061a4a..00000000000 --- a/spec/frontend/ide/file_helpers.js +++ /dev/null @@ -1,36 +0,0 @@ -export const createFile = (path, content = '') => ({ - id: path, - path, - content, - raw: content, -}); - -export const createNewFile = (path, content) => - Object.assign(createFile(path, content), { - tempFile: true, - raw: '', - }); - -export const createDeletedFile = (path, content) => - Object.assign(createFile(path, content), { - deleted: true, - }); - -export const createUpdatedFile = (path, oldContent, content) => - Object.assign(createFile(path, content), { - raw: oldContent, - }); - -export const createMovedFile = (path, prevPath, content) => - Object.assign(createNewFile(path, content), { - prevPath, - }); - -export const createEntries = (path) => - // eslint-disable-next-line max-params - path.split('/').reduce((acc, part, idx, parts) => { - const parentPath = parts.slice(0, idx).join('/'); - const fullPath = parentPath ? `${parentPath}/${part}` : part; - - return Object.assign(acc, { [fullPath]: { ...createFile(fullPath), parentPath } }); - }, {}); diff --git a/spec/frontend/ide/helpers.js b/spec/frontend/ide/helpers.js index 89d3b14c432..048a2d820d3 100644 --- a/spec/frontend/ide/helpers.js +++ b/spec/frontend/ide/helpers.js @@ -1,5 +1,4 @@ -import * as pathUtils from 'path'; -import { WEB_IDE_OAUTH_CALLBACK_URL_PATH, commitActionTypes } from '~/ide/constants'; +import { WEB_IDE_OAUTH_CALLBACK_URL_PATH } from '~/ide/constants'; import { decorateData } from '~/ide/stores/utils'; // eslint-disable-next-line max-params @@ -13,42 +12,5 @@ export const file = (name = 'name', id = name, type = '', parent = null) => parentPath: parent ? parent.path : '', }); -export const createEntriesFromPaths = (paths) => - paths - .map((path) => ({ - name: pathUtils.basename(path), - dir: pathUtils.dirname(path), - ext: pathUtils.extname(path), - })) - .reduce((entries, path, idx) => { - const { name } = path; - const parent = path.dir ? entries[path.dir] : null; - const type = path.ext ? 'blob' : 'tree'; - const entry = file(name, (idx + 1).toString(), type, parent); - return { - [entry.path]: entry, - ...entries, - }; - }, {}); - -export const createTriggerChangeAction = (payload) => ({ - type: 'triggerFilesChange', - ...(payload ? { payload } : {}), -}); - -export const createTriggerRenamePayload = (path, newPath) => ({ - type: commitActionTypes.move, - path, - newPath, -}); - -export const createTriggerUpdatePayload = (path) => ({ - type: commitActionTypes.update, - path, -}); - -export const createTriggerRenameAction = (path, newPath) => - createTriggerChangeAction(createTriggerRenamePayload(path, newPath)); - export const getMockCallbackUrl = () => new URL(WEB_IDE_OAUTH_CALLBACK_URL_PATH, window.location.origin).toString(); diff --git a/spec/frontend/ide/ide_router_extension_spec.js b/spec/frontend/ide/ide_router_extension_spec.js deleted file mode 100644 index 9f77a0d29f9..00000000000 --- a/spec/frontend/ide/ide_router_extension_spec.js +++ /dev/null @@ -1,53 +0,0 @@ -import VueRouter from 'vue-router'; -import { describeSkipVue3, SkipReason } from 'helpers/vue3_conditional'; -import IdeRouter from '~/ide/ide_router_extension'; - -jest.mock('vue-router'); -const skipReason = new SkipReason({ - name: 'IDE overrides of VueRouter', - reason: 'Legacy WebIDE is due for deletion', - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/508949', -}); -describeSkipVue3(skipReason, () => { - const paths = (branch) => [ - `${branch}`, - `/${branch}`, - `/${branch}/-/`, - `/edit/${branch}`, - `/edit/${branch}/-/`, - `/blob/${branch}`, - `/blob/${branch}/-/`, - `/blob/${branch}/-/src/merge_requests/2`, - `/blob/${branch}/-/src/blob/`, - `/tree/${branch}/-/src/blob/`, - `/tree/${branch}/-/src/tree/`, - ]; - let router; - - beforeEach(() => { - VueRouter.mockClear(); - router = new IdeRouter({ - mode: 'history', - }); - }); - - it.each` - path | expected - ${'#-test'} | ${'%23-test'} - ${'#test'} | ${'%23test'} - ${'test#'} | ${'test%23'} - ${'test-#'} | ${'test-%23'} - ${'test-#-hash'} | ${'test-%23-hash'} - ${'test/hash#123'} | ${'test/hash%23123'} - `('finds project path when route is $path', ({ path, expected }) => { - paths(path).forEach((route) => { - const expectedPath = route.replace(path, expected); - - router.push(route); - expect(VueRouter.prototype.push).toHaveBeenCalledWith(expectedPath, undefined, undefined); - - router.resolve(route); - expect(VueRouter.prototype.resolve).toHaveBeenCalledWith(expectedPath, undefined, undefined); - }); - }); -}); diff --git a/spec/frontend/ide/ide_router_spec.js b/spec/frontend/ide/ide_router_spec.js deleted file mode 100644 index 7fbe8fa8c26..00000000000 --- a/spec/frontend/ide/ide_router_spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import waitForPromises from 'helpers/wait_for_promises'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import { describeSkipVue3, SkipReason } from 'helpers/vue3_conditional'; -import { createRouter } from '~/ide/ide_router'; -import { createStore } from '~/ide/stores'; - -const skipReason = new SkipReason({ - name: 'IDE router', - reason: 'Legacy WebIDE is due for deletion', - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/508949', -}); -describeSkipVue3(skipReason, () => { - const PROJECT_NAMESPACE = 'my-group/sub-group'; - const PROJECT_NAME = 'my-project'; - const TEST_PATH = `/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/2`; - const DEFAULT_BRANCH = 'default-main'; - - let store; - let router; - - beforeEach(() => { - stubPerformanceWebAPI(); - - window.history.replaceState({}, '', '/'); - store = createStore(); - router = createRouter(store, DEFAULT_BRANCH); - jest.spyOn(store, 'dispatch').mockReturnValue(new Promise(() => {})); - }); - - it.each` - route | expectedBranchId | expectedBasePath - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob/`} | ${'main'} | ${'src/blob/'} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/blob`} | ${'main'} | ${'src/blob'} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob/-/src/blob`} | ${'blob'} | ${'src/blob'} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/main/-/src/tree/`} | ${'main'} | ${'src/tree/'} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/weird:branch/name-123/-/src/tree/`} | ${'weird:branch/name-123'} | ${'src/tree/'} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/blob`} | ${'main'} | ${'src/blob'} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/edit`} | ${'main'} | ${'src/edit'} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/main/-/src/merge_requests/2`} | ${'main'} | ${'src/merge_requests/2'} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/blob/blob/-/src/blob`} | ${'blob'} | ${'src/blob'} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit/blob/-/src/blob`} | ${'blob'} | ${'src/blob'} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/tree/blob`} | ${'blob'} | ${''} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/edit`} | ${DEFAULT_BRANCH} | ${''} - ${`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}`} | ${DEFAULT_BRANCH} | ${''} - `('correctly opens Web IDE for $route', ({ route, expectedBranchId, expectedBasePath } = {}) => { - router.push(route); - - expect(store.dispatch).toHaveBeenCalledWith('openBranch', { - projectId: `${PROJECT_NAMESPACE}/${PROJECT_NAME}`, - branchId: expectedBranchId, - basePath: expectedBasePath, - }); - }); - - it('correctly opens an MR', () => { - const expectedId = '2'; - - router.push(`/project/${PROJECT_NAMESPACE}/${PROJECT_NAME}/merge_requests/${expectedId}`); - - expect(store.dispatch).toHaveBeenCalledWith('openMergeRequest', { - projectId: `${PROJECT_NAMESPACE}/${PROJECT_NAME}`, - mergeRequestId: expectedId, - targetProjectId: undefined, - }); - expect(store.dispatch).not.toHaveBeenCalledWith('openBranch'); - }); - - it('keeps router in sync when store changes', async () => { - expect(router.currentRoute.fullPath).toBe('/'); - - store.state.router.fullPath = TEST_PATH; - - await waitForPromises(); - - expect(router.currentRoute.fullPath).toBe(TEST_PATH); - }); - - it('keeps store in sync when router changes', () => { - expect(store.dispatch).not.toHaveBeenCalled(); - - router.push(TEST_PATH); - - expect(store.dispatch).toHaveBeenCalledWith('router/push', TEST_PATH, { root: true }); - }); -}); diff --git a/spec/frontend/ide/index_spec.js b/spec/frontend/ide/index_spec.js index d9027a4e9c8..ccffda7608e 100644 --- a/spec/frontend/ide/index_spec.js +++ b/spec/frontend/ide/index_spec.js @@ -6,7 +6,6 @@ import setWindowLocation from 'helpers/set_window_location_helper'; import { TEST_HOST } from 'helpers/test_constants'; jest.mock('~/ide/init_gitlab_web_ide'); -jest.mock('~/ide/init_legacy_web_ide'); const MOCK_MISMATCH_CALLBACK_URL = 'https://example.com/ide/redirect'; diff --git a/spec/frontend/ide/lib/common/disposable_spec.js b/spec/frontend/ide/lib/common/disposable_spec.js deleted file mode 100644 index 8596642eb7a..00000000000 --- a/spec/frontend/ide/lib/common/disposable_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import Disposable from '~/ide/lib/common/disposable'; - -describe('Multi-file editor library disposable class', () => { - let instance; - let disposableClass; - - beforeEach(() => { - instance = new Disposable(); - - disposableClass = { - dispose: jest.fn().mockName('dispose'), - }; - }); - - afterEach(() => { - instance.dispose(); - }); - - describe('add', () => { - it('adds disposable classes', () => { - instance.add(disposableClass); - - expect(instance.disposers.size).toBe(1); - }); - }); - - describe('dispose', () => { - beforeEach(() => { - instance.add(disposableClass); - }); - - it('calls dispose on all cached disposers', () => { - instance.dispose(); - - expect(disposableClass.dispose).toHaveBeenCalled(); - }); - - it('clears cached disposers', () => { - instance.dispose(); - - expect(instance.disposers.size).toBe(0); - }); - }); -}); diff --git a/spec/frontend/ide/lib/common/model_manager_spec.js b/spec/frontend/ide/lib/common/model_manager_spec.js deleted file mode 100644 index e485873e8da..00000000000 --- a/spec/frontend/ide/lib/common/model_manager_spec.js +++ /dev/null @@ -1,126 +0,0 @@ -import eventHub from '~/ide/eventhub'; -import ModelManager from '~/ide/lib/common/model_manager'; -import { file } from '../../helpers'; - -describe('Multi-file editor library model manager', () => { - let instance; - - beforeEach(() => { - instance = new ModelManager(); - }); - - afterEach(() => { - instance.dispose(); - }); - - describe('addModel', () => { - it('caches model', () => { - instance.addModel(file()); - - expect(instance.models.size).toBe(1); - }); - - it('caches model by file path', () => { - const f = file('path-name'); - instance.addModel(f); - - expect(instance.models.keys().next().value).toBe(f.key); - }); - - it('adds model into disposable', () => { - jest.spyOn(instance.disposable, 'add'); - - instance.addModel(file()); - - expect(instance.disposable.add).toHaveBeenCalled(); - }); - - it('returns cached model', () => { - jest.spyOn(instance.models, 'get'); - - instance.addModel(file()); - instance.addModel(file()); - - expect(instance.models.get).toHaveBeenCalled(); - }); - - it('adds eventHub listener', () => { - const f = file(); - jest.spyOn(eventHub, '$on'); - - instance.addModel(f); - - expect(eventHub.$on).toHaveBeenCalledWith( - `editor.update.model.dispose.${f.key}`, - expect.anything(), - ); - }); - }); - - describe('hasCachedModel', () => { - it('returns false when no models exist', () => { - expect(instance.hasCachedModel('path')).toBe(false); - }); - - it('returns true when model exists', () => { - const f = file('path-name'); - - instance.addModel(f); - - expect(instance.hasCachedModel(f.key)).toBe(true); - }); - }); - - describe('getModel', () => { - it('returns cached model', () => { - instance.addModel(file('path-name')); - - expect(instance.getModel('path-name')).not.toBeNull(); - }); - }); - - describe('removeCachedModel', () => { - let f; - - beforeEach(() => { - f = file(); - - instance.addModel(f); - }); - - it('clears cached model', () => { - instance.removeCachedModel(f); - - expect(instance.models.size).toBe(0); - }); - - it('removes eventHub listener', () => { - jest.spyOn(eventHub, '$off'); - - instance.removeCachedModel(f); - - expect(eventHub.$off).toHaveBeenCalledWith( - `editor.update.model.dispose.${f.key}`, - expect.anything(), - ); - }); - }); - - describe('dispose', () => { - it('clears cached models', () => { - instance.addModel(file()); - - instance.dispose(); - - expect(instance.models.size).toBe(0); - }); - - it('calls disposable dispose', () => { - jest.spyOn(instance.disposable, 'dispose'); - - instance.dispose(); - - expect(instance.disposable.dispose).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/frontend/ide/lib/common/model_spec.js b/spec/frontend/ide/lib/common/model_spec.js deleted file mode 100644 index 39c50f628c2..00000000000 --- a/spec/frontend/ide/lib/common/model_spec.js +++ /dev/null @@ -1,201 +0,0 @@ -import eventHub from '~/ide/eventhub'; -import Model from '~/ide/lib/common/model'; -import { file } from '../../helpers'; - -describe('Multi-file editor library model', () => { - let model; - - beforeEach(() => { - jest.spyOn(eventHub, '$on'); - - const f = file('path'); - f.mrChange = { diff: 'ABC' }; - f.baseRaw = 'test'; - model = new Model(f); - }); - - afterEach(() => { - model.dispose(); - }); - - it('creates original model & base model & new model', () => { - expect(model.originalModel).not.toBeNull(); - expect(model.model).not.toBeNull(); - expect(model.baseModel).not.toBeNull(); - - expect(model.originalModel.uri.path).toBe('original/path--path'); - expect(model.model.uri.path).toBe('path--path'); - expect(model.baseModel.uri.path).toBe('target/path--path'); - }); - - it('creates model with head file to compare against', () => { - const f = file('path'); - model.dispose(); - - model = new Model(f, { - ...f, - content: '123 testing', - }); - - expect(model.head).not.toBeNull(); - expect(model.getOriginalModel().getValue()).toBe('123 testing'); - }); - - it('adds eventHub listener', () => { - expect(eventHub.$on).toHaveBeenCalledWith( - `editor.update.model.dispose.${model.file.key}`, - expect.anything(), - ); - }); - - describe('path', () => { - it('returns file path', () => { - expect(model.path).toBe(model.file.key); - }); - }); - - describe('getModel', () => { - it('returns model', () => { - expect(model.getModel()).toBe(model.model); - }); - }); - - describe('getOriginalModel', () => { - it('returns original model', () => { - expect(model.getOriginalModel()).toBe(model.originalModel); - }); - }); - - describe('getBaseModel', () => { - it('returns base model', () => { - expect(model.getBaseModel()).toBe(model.baseModel); - }); - }); - - describe('setValue', () => { - it('updates models value', () => { - model.setValue('testing 123'); - - expect(model.getModel().getValue()).toBe('testing 123'); - }); - }); - - describe('onChange', () => { - it('calls callback on change', () => { - const spy = jest.fn(); - model.onChange(spy); - - model.getModel().setValue('123'); - - expect(spy).toHaveBeenCalledWith(model, expect.anything()); - }); - }); - - describe('dispose', () => { - it('calls disposable dispose', () => { - jest.spyOn(model.disposable, 'dispose'); - - model.dispose(); - - expect(model.disposable.dispose).toHaveBeenCalled(); - }); - - it('clears events', () => { - model.onChange(() => {}); - - expect(model.events.size).toBe(1); - - model.dispose(); - - expect(model.events.size).toBe(0); - }); - - it('removes eventHub listener', () => { - jest.spyOn(eventHub, '$off'); - - model.dispose(); - - expect(eventHub.$off).toHaveBeenCalledWith( - `editor.update.model.dispose.${model.file.key}`, - expect.anything(), - ); - }); - - it('calls onDispose callback', () => { - const disposeSpy = jest.fn(); - - model.onDispose(disposeSpy); - - model.dispose(); - - expect(disposeSpy).toHaveBeenCalled(); - }); - - it('applies custom options and triggers onChange callback', () => { - const changeSpy = jest.fn(); - jest.spyOn(model, 'applyCustomOptions'); - - model.onChange(changeSpy); - - model.dispose(); - - expect(model.applyCustomOptions).toHaveBeenCalled(); - expect(changeSpy).toHaveBeenCalled(); - }); - }); - - describe('updateOptions', () => { - it('sets the options on the options object', () => { - model.updateOptions({ insertSpaces: true, someOption: 'some value' }); - - expect(model.options).toEqual({ - insertFinalNewline: true, - insertSpaces: true, - someOption: 'some value', - trimTrailingWhitespace: false, - }); - }); - - it.each` - option | value - ${'insertSpaces'} | ${true} - ${'insertSpaces'} | ${false} - ${'indentSize'} | ${4} - ${'tabSize'} | ${3} - `("correctly sets option: $option=$value to Monaco's TextModel", ({ option, value }) => { - model.updateOptions({ [option]: value }); - - expect(model.getModel().getOptions()).toMatchObject({ [option]: value }); - }); - - it('applies custom options immediately', () => { - jest.spyOn(model, 'applyCustomOptions'); - - model.updateOptions({ trimTrailingWhitespace: true, someOption: 'some value' }); - - expect(model.applyCustomOptions).toHaveBeenCalled(); - }); - }); - - describe('applyCustomOptions', () => { - it.each` - option | value | contentBefore | contentAfter - ${'insertFinalNewline'} | ${true} | ${'hello\nworld'} | ${'hello\nworld\n'} - ${'insertFinalNewline'} | ${true} | ${'hello\nworld\n'} | ${'hello\nworld\n'} - ${'insertFinalNewline'} | ${false} | ${'hello\nworld'} | ${'hello\nworld'} - ${'trimTrailingWhitespace'} | ${true} | ${'hello \t\nworld \t\n'} | ${'hello\nworld\n'} - ${'trimTrailingWhitespace'} | ${true} | ${'hello \t\r\nworld \t\r\n'} | ${'hello\r\nworld\r\n'} - ${'trimTrailingWhitespace'} | ${false} | ${'hello \t\r\nworld \t\r\n'} | ${'hello \t\r\nworld \t\r\n'} - `( - 'correctly applies custom option $option=$value to content', - ({ option, value, contentBefore, contentAfter }) => { - model.options[option] = value; - - model.updateNewContent(contentBefore); - model.applyCustomOptions(); - - expect(model.getModel().getValue()).toEqual(contentAfter); - }, - ); - }); -}); diff --git a/spec/frontend/ide/lib/create_diff_spec.js b/spec/frontend/ide/lib/create_diff_spec.js deleted file mode 100644 index b33fa599d1c..00000000000 --- a/spec/frontend/ide/lib/create_diff_spec.js +++ /dev/null @@ -1,182 +0,0 @@ -import { commitActionTypes } from '~/ide/constants'; -import createDiff from '~/ide/lib/create_diff'; -import createFileDiff from '~/ide/lib/create_file_diff'; -import { - createNewFile, - createUpdatedFile, - createDeletedFile, - createMovedFile, - createEntries, -} from '../file_helpers'; - -const PATH_FOO = 'test/foo.md'; -const PATH_BAR = 'test/bar.md'; -const PATH_ZED = 'test/zed.md'; -const PATH_LOREM = 'test/lipsum/nested/lorem.md'; -const PATH_IPSUM = 'test/lipsum/ipsum.md'; -const TEXT = `Lorem ipsum dolor sit amet, -consectetur adipiscing elit. -Morbi ex dolor, euismod nec rutrum nec, egestas at ligula. -Praesent scelerisque ut nisi eu eleifend. -Suspendisse potenti. -`; -const LINES = TEXT.trim().split('\n'); - -const joinDiffs = (...patches) => patches.join(''); - -describe('IDE lib/create_diff', () => { - it('with created files, generates patch', () => { - const changedFiles = [createNewFile(PATH_FOO, TEXT), createNewFile(PATH_BAR, '')]; - const result = createDiff({ changedFiles }); - - expect(result).toEqual({ - patch: joinDiffs( - createFileDiff(changedFiles[0], commitActionTypes.create), - createFileDiff(changedFiles[1], commitActionTypes.create), - ), - toDelete: [], - }); - }); - - it('with deleted files, adds to delete', () => { - const changedFiles = [createDeletedFile(PATH_FOO, TEXT), createDeletedFile(PATH_BAR, '')]; - - const result = createDiff({ changedFiles }); - - expect(result).toEqual({ - patch: '', - toDelete: [PATH_FOO, PATH_BAR], - }); - }); - - it('with updated files, generates patch', () => { - const changedFiles = [createUpdatedFile(PATH_FOO, TEXT, 'A change approaches!')]; - - const result = createDiff({ changedFiles }); - - expect(result).toEqual({ - patch: createFileDiff(changedFiles[0], commitActionTypes.update), - toDelete: [], - }); - }); - - it('with files in both staged and changed, prefer changed', () => { - const changedFiles = [ - createUpdatedFile(PATH_FOO, TEXT, 'Do a change!'), - createDeletedFile(PATH_LOREM), - ]; - - const result = createDiff({ - changedFiles, - stagedFiles: [createUpdatedFile(PATH_LOREM, TEXT, ''), createDeletedFile(PATH_FOO, TEXT)], - }); - - expect(result).toEqual({ - patch: createFileDiff(changedFiles[0], commitActionTypes.update), - toDelete: [PATH_LOREM], - }); - }); - - it('with file created in staging and deleted in changed, do nothing', () => { - const result = createDiff({ - changedFiles: [createDeletedFile(PATH_FOO)], - stagedFiles: [createNewFile(PATH_FOO, TEXT)], - }); - - expect(result).toEqual({ - patch: '', - toDelete: [], - }); - }); - - it('with file deleted in both staged and changed, delete', () => { - const result = createDiff({ - changedFiles: [createDeletedFile(PATH_LOREM)], - stagedFiles: [createDeletedFile(PATH_LOREM)], - }); - - expect(result).toEqual({ - patch: '', - toDelete: [PATH_LOREM], - }); - }); - - it('with file moved, create and delete', () => { - const changedFiles = [createMovedFile(PATH_BAR, PATH_FOO, TEXT)]; - - const result = createDiff({ - changedFiles, - stagedFiles: [createDeletedFile(PATH_FOO)], - }); - - expect(result).toEqual({ - patch: createFileDiff(changedFiles[0], commitActionTypes.create), - toDelete: [PATH_FOO], - }); - }); - - it('with file moved and no content, move', () => { - const changedFiles = [createMovedFile(PATH_BAR, PATH_FOO)]; - - const result = createDiff({ - changedFiles, - stagedFiles: [createDeletedFile(PATH_FOO)], - }); - - expect(result).toEqual({ - patch: createFileDiff(changedFiles[0], commitActionTypes.move), - toDelete: [], - }); - }); - - it('creates a well formatted patch', () => { - const changedFiles = [ - createMovedFile(PATH_BAR, PATH_FOO), - createDeletedFile(PATH_ZED), - createNewFile(PATH_LOREM, TEXT), - createUpdatedFile(PATH_IPSUM, TEXT, "That's all folks!"), - ]; - - const expectedPatch = `diff --git "a/${PATH_FOO}" "b/${PATH_BAR}" -rename from ${PATH_FOO} -rename to ${PATH_BAR} -diff --git "a/${PATH_LOREM}" "b/${PATH_LOREM}" -new file mode 100644 ---- /dev/null -+++ b/${PATH_LOREM} -@@ -0,0 +1,${LINES.length} @@ -${LINES.map((line) => `+${line}`).join('\n')} -diff --git "a/${PATH_IPSUM}" "b/${PATH_IPSUM}" ---- a/${PATH_IPSUM} -+++ b/${PATH_IPSUM} -@@ -1,${LINES.length} +1,1 @@ -${LINES.map((line) => `-${line}`).join('\n')} -+That's all folks! -\\ No newline at end of file -`; - - const result = createDiff({ changedFiles }); - - expect(result).toEqual({ - patch: expectedPatch, - toDelete: [PATH_ZED], - }); - }); - - it('deletes deleted parent directories', () => { - const deletedFiles = ['foo/bar/zed/test.md', 'foo/bar/zed/test2.md']; - const entries = deletedFiles.reduce((acc, path) => Object.assign(acc, createEntries(path)), {}); - const allDeleted = [...deletedFiles, 'foo/bar/zed', 'foo/bar']; - allDeleted.forEach((path) => { - entries[path].deleted = true; - }); - const changedFiles = deletedFiles.map((x) => entries[x]); - - const result = createDiff({ changedFiles, entries }); - - expect(result).toEqual({ - patch: '', - toDelete: allDeleted, - }); - }); -}); diff --git a/spec/frontend/ide/lib/create_file_diff_spec.js b/spec/frontend/ide/lib/create_file_diff_spec.js deleted file mode 100644 index 4cb5bb56cb2..00000000000 --- a/spec/frontend/ide/lib/create_file_diff_spec.js +++ /dev/null @@ -1,160 +0,0 @@ -import { commitActionTypes } from '~/ide/constants'; -import createFileDiff from '~/ide/lib/create_file_diff'; -import { - createUpdatedFile, - createNewFile, - createMovedFile, - createDeletedFile, -} from '../file_helpers'; - -const PATH = 'test/numbers.md'; -const PATH_FOO = 'test/foo.md'; -const TEXT_LINE_COUNT = 100; -const TEXT = Array(TEXT_LINE_COUNT) - .fill(0) - .map((_, idx) => `${idx + 1}`) - .join('\n'); - -// eslint-disable-next-line max-params -const spliceLines = (content, lineNumber, deleteCount = 0, newLines = []) => { - const lines = content.split('\n'); - lines.splice(lineNumber, deleteCount, ...newLines); - return lines.join('\n'); -}; - -const mapLines = (content, mapFn) => content.split('\n').map(mapFn).join('\n'); - -describe('IDE lib/create_file_diff', () => { - it('returns empty string with "garbage" action', () => { - const result = createFileDiff(createNewFile(PATH, ''), 'garbage'); - - expect(result).toBe(''); - }); - - it('preserves ending whitespace in file', () => { - const oldContent = spliceLines(TEXT, 99, 1, ['100 ']); - const newContent = spliceLines(oldContent, 99, 0, ['Lorem', 'Ipsum']); - const expected = ` - 99 -+Lorem -+Ipsum - 100 `; - - const result = createFileDiff( - createUpdatedFile(PATH, oldContent, newContent), - commitActionTypes.update, - ); - - expect(result).toContain(expected); - }); - - describe('with "create" action', () => { - const expectedHead = `diff --git "a/${PATH}" "b/${PATH}" -new file mode 100644`; - - const expectedChunkHead = (lineCount) => `--- /dev/null -+++ b/${PATH} -@@ -0,0 +1,${lineCount} @@`; - - it('with empty file, does not include diff body', () => { - const result = createFileDiff(createNewFile(PATH, ''), commitActionTypes.create); - - expect(result).toBe(`${expectedHead}\n`); - }); - - it('with single line, includes diff body', () => { - const result = createFileDiff(createNewFile(PATH, '\n'), commitActionTypes.create); - - expect(result).toBe(`${expectedHead} -${expectedChunkHead(1)} -+ -`); - }); - - it('without newline, includes no newline comment', () => { - const result = createFileDiff(createNewFile(PATH, 'Lorem ipsum'), commitActionTypes.create); - - expect(result).toBe(`${expectedHead} -${expectedChunkHead(1)} -+Lorem ipsum -\\ No newline at end of file -`); - }); - - it('with content, includes diff body', () => { - const content = `${TEXT}\n`; - const result = createFileDiff(createNewFile(PATH, content), commitActionTypes.create); - - expect(result).toBe(`${expectedHead} -${expectedChunkHead(TEXT_LINE_COUNT)} -${mapLines(TEXT, (line) => `+${line}`)} -`); - }); - }); - - describe('with "delete" action', () => { - const expectedHead = `diff --git "a/${PATH}" "b/${PATH}" -deleted file mode 100644`; - - const expectedChunkHead = (lineCount) => `--- a/${PATH} -+++ /dev/null -@@ -1,${lineCount} +0,0 @@`; - - it('with empty file, does not include diff body', () => { - const result = createFileDiff(createDeletedFile(PATH, ''), commitActionTypes.delete); - - expect(result).toBe(`${expectedHead}\n`); - }); - - it('with content, includes diff body', () => { - const content = `${TEXT}\n`; - const result = createFileDiff(createDeletedFile(PATH, content), commitActionTypes.delete); - - expect(result).toBe(`${expectedHead} -${expectedChunkHead(TEXT_LINE_COUNT)} -${mapLines(TEXT, (line) => `-${line}`)} -`); - }); - }); - - describe('with "update" action', () => { - it('includes diff body', () => { - const oldContent = `${TEXT}\n`; - const newContent = `${spliceLines(TEXT, 50, 3, ['Lorem'])}\n`; - - const result = createFileDiff( - createUpdatedFile(PATH, oldContent, newContent), - commitActionTypes.update, - ); - - expect(result).toBe(`diff --git "a/${PATH}" "b/${PATH}" ---- a/${PATH} -+++ b/${PATH} -@@ -47,11 +47,9 @@ - 47 - 48 - 49 - 50 --51 --52 --53 -+Lorem - 54 - 55 - 56 - 57 -`); - }); - }); - - describe('with "move" action', () => { - it('returns rename head', () => { - const result = createFileDiff(createMovedFile(PATH, PATH_FOO), commitActionTypes.move); - - expect(result).toBe(`diff --git "a/${PATH_FOO}" "b/${PATH}" -rename from ${PATH_FOO} -rename to ${PATH} -`); - }); - }); -}); diff --git a/spec/frontend/ide/lib/decorations/controller_spec.js b/spec/frontend/ide/lib/decorations/controller_spec.js deleted file mode 100644 index b513f1b2eba..00000000000 --- a/spec/frontend/ide/lib/decorations/controller_spec.js +++ /dev/null @@ -1,146 +0,0 @@ -import Model from '~/ide/lib/common/model'; -import DecorationsController from '~/ide/lib/decorations/controller'; -import Editor from '~/ide/lib/editor'; -import { createStore } from '~/ide/stores'; -import { file } from '../../helpers'; - -describe('Multi-file editor library decorations controller', () => { - let editorInstance; - let controller; - let model; - let store; - - beforeEach(() => { - store = createStore(); - editorInstance = Editor.create(store); - editorInstance.createInstance(document.createElement('div')); - - controller = new DecorationsController(editorInstance); - model = new Model(file('path')); - }); - - afterEach(() => { - model.dispose(); - editorInstance.dispose(); - controller.dispose(); - }); - - describe('getAllDecorationsForModel', () => { - it('returns empty array when no decorations exist for model', () => { - const decorations = controller.getAllDecorationsForModel(model); - - expect(decorations).toEqual([]); - }); - - it('returns decorations by model URL', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - const decorations = controller.getAllDecorationsForModel(model); - - expect(decorations[0]).toEqual({ decoration: 'decorationValue' }); - }); - }); - - describe('addDecorations', () => { - it('caches decorations in a new map', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - expect(controller.decorations.size).toBe(1); - }); - - it('does not create new cache model', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]); - - expect(controller.decorations.size).toBe(1); - }); - - it('caches decorations by model URL', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - expect(controller.decorations.size).toBe(1); - expect(controller.decorations.keys().next().value).toBe('gitlab:path--path'); - }); - - it('calls decorate method', () => { - jest.spyOn(controller, 'decorate').mockImplementation(() => {}); - - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - expect(controller.decorate).toHaveBeenCalled(); - }); - }); - - describe('decorate', () => { - it('sets decorations on editor instance', () => { - jest.spyOn(controller.editor.instance, 'deltaDecorations').mockImplementation(() => {}); - - controller.decorate(model); - - expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []); - }); - - it('caches decorations', () => { - jest.spyOn(controller.editor.instance, 'deltaDecorations').mockReturnValue([]); - - controller.decorate(model); - - expect(controller.editorDecorations.size).toBe(1); - }); - - it('caches decorations by model URL', () => { - jest.spyOn(controller.editor.instance, 'deltaDecorations').mockReturnValue([]); - - controller.decorate(model); - - expect(controller.editorDecorations.keys().next().value).toBe('gitlab:path--path'); - }); - }); - - describe('dispose', () => { - it('clears cached decorations', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - controller.dispose(); - - expect(controller.decorations.size).toBe(0); - }); - - it('clears cached editorDecorations', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - controller.dispose(); - - expect(controller.editorDecorations.size).toBe(0); - }); - }); - - describe('hasDecorations', () => { - it('returns true when decorations are cached', () => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - - expect(controller.hasDecorations(model)).toBe(true); - }); - - it('returns false when no model decorations exist', () => { - expect(controller.hasDecorations(model)).toBe(false); - }); - }); - - describe('removeDecorations', () => { - beforeEach(() => { - controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]); - controller.decorate(model); - }); - - it('removes cached decorations', () => { - expect(controller.decorations.size).not.toBe(0); - expect(controller.editorDecorations.size).not.toBe(0); - - controller.removeDecorations(model); - - expect(controller.decorations.size).toBe(0); - expect(controller.editorDecorations.size).toBe(0); - }); - }); -}); diff --git a/spec/frontend/ide/lib/diff/controller_spec.js b/spec/frontend/ide/lib/diff/controller_spec.js deleted file mode 100644 index 5f1344f1ea2..00000000000 --- a/spec/frontend/ide/lib/diff/controller_spec.js +++ /dev/null @@ -1,219 +0,0 @@ -import { Range } from 'monaco-editor'; -import ModelManager from '~/ide/lib/common/model_manager'; -import DecorationsController from '~/ide/lib/decorations/controller'; -import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller'; -import { computeDiff } from '~/ide/lib/diff/diff'; -import Editor from '~/ide/lib/editor'; -import { createStore } from '~/ide/stores'; -import { file } from '../../helpers'; - -describe('Multi-file editor library dirty diff controller', () => { - let editorInstance; - let controller; - let modelManager; - let decorationsController; - let model; - let store; - - beforeEach(() => { - store = createStore(); - - editorInstance = Editor.create(store); - editorInstance.createInstance(document.createElement('div')); - - modelManager = new ModelManager(); - decorationsController = new DecorationsController(editorInstance); - - model = modelManager.addModel(file('path')); - - controller = new DirtyDiffController(modelManager, decorationsController); - }); - - afterEach(() => { - controller.dispose(); - model.dispose(); - decorationsController.dispose(); - editorInstance.dispose(); - }); - - describe('getDiffChangeType', () => { - ['added', 'removed', 'modified'].forEach((type) => { - it(`returns ${type}`, () => { - const change = { - [type]: true, - }; - - expect(getDiffChangeType(change)).toBe(type); - }); - }); - }); - - describe('getDecorator', () => { - ['added', 'removed', 'modified'].forEach((type) => { - it(`returns with linesDecorationsClassName for ${type}`, () => { - const change = { - [type]: true, - }; - - expect(getDecorator(change).options.linesDecorationsClassName).toBe( - `dirty-diff dirty-diff-${type}`, - ); - }); - - it('returns with line numbers', () => { - const change = { - lineNumber: 1, - endLineNumber: 2, - [type]: true, - }; - - const { range } = getDecorator(change); - - expect(range.startLineNumber).toBe(1); - expect(range.endLineNumber).toBe(2); - expect(range.startColumn).toBe(1); - expect(range.endColumn).toBe(1); - }); - }); - }); - - describe('attachModel', () => { - it('adds change event callback', () => { - jest.spyOn(model, 'onChange').mockImplementation(() => {}); - - controller.attachModel(model); - - expect(model.onChange).toHaveBeenCalled(); - }); - - it('adds dispose event callback', () => { - jest.spyOn(model, 'onDispose').mockImplementation(() => {}); - - controller.attachModel(model); - - expect(model.onDispose).toHaveBeenCalled(); - }); - - it('calls throttledComputeDiff on change', () => { - jest.spyOn(controller, 'throttledComputeDiff').mockImplementation(() => {}); - - controller.attachModel(model); - - model.getModel().setValue('123'); - - expect(controller.throttledComputeDiff).toHaveBeenCalled(); - }); - - it('caches model', () => { - controller.attachModel(model); - - expect(controller.models.has(model.url)).toBe(true); - }); - }); - - describe('computeDiff', () => { - it('posts to worker', () => { - jest.spyOn(controller.dirtyDiffWorker, 'postMessage').mockImplementation(() => {}); - - controller.computeDiff(model); - - expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({ - path: model.path, - originalContent: '', - newContent: '', - }); - }); - }); - - describe('reDecorate', () => { - it('calls computeDiff when no decorations are cached', () => { - jest.spyOn(controller, 'computeDiff').mockImplementation(() => {}); - - controller.reDecorate(model); - - expect(controller.computeDiff).toHaveBeenCalledWith(model); - }); - - it('calls decorate when decorations are cached', () => { - jest.spyOn(controller.decorationsController, 'decorate').mockImplementation(() => {}); - - controller.decorationsController.decorations.set(model.url, 'test'); - - controller.reDecorate(model); - - expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model); - }); - }); - - describe('decorate', () => { - it('adds decorations into decorations controller', () => { - jest.spyOn(controller.decorationsController, 'addDecorations').mockImplementation(() => {}); - - controller.decorate({ data: { changes: [], path: model.path } }); - - expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith( - model, - 'dirtyDiff', - expect.anything(), - ); - }); - - it('adds decorations into editor', () => { - const spy = jest.spyOn(controller.decorationsController.editor.instance, 'deltaDecorations'); - - controller.decorate({ - data: { changes: computeDiff('123', '1234'), path: model.path }, - }); - - expect(spy).toHaveBeenCalledWith( - [], - [ - { - range: new Range(1, 1, 1, 1), - options: { - isWholeLine: true, - linesDecorationsClassName: 'dirty-diff dirty-diff-modified', - }, - }, - ], - ); - }); - }); - - describe('dispose', () => { - it('calls disposable dispose', () => { - jest.spyOn(controller.disposable, 'dispose'); - - controller.dispose(); - - expect(controller.disposable.dispose).toHaveBeenCalled(); - }); - - it('terminates worker', () => { - jest.spyOn(controller.dirtyDiffWorker, 'terminate'); - - controller.dispose(); - - expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled(); - }); - - it('removes worker event listener', () => { - jest.spyOn(controller.dirtyDiffWorker, 'removeEventListener'); - - controller.dispose(); - - expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith( - 'message', - expect.anything(), - ); - }); - - it('clears cached models', () => { - controller.attachModel(model); - - model.dispose(); - - expect(controller.models.size).toBe(0); - }); - }); -}); diff --git a/spec/frontend/ide/lib/diff/diff_spec.js b/spec/frontend/ide/lib/diff/diff_spec.js deleted file mode 100644 index 208ed9bf759..00000000000 --- a/spec/frontend/ide/lib/diff/diff_spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import { computeDiff } from '~/ide/lib/diff/diff'; - -describe('Multi-file editor library diff calculator', () => { - describe('computeDiff', () => { - it('returns empty array if no changes', () => { - const diff = computeDiff('123', '123'); - - expect(diff).toEqual([]); - }); - - describe('modified', () => { - it.each` - originalContent | newContent | lineNumber - ${'123'} | ${'1234'} | ${1} - ${'123\n123\n123'} | ${'123\n1234\n123'} | ${2} - `( - 'marks line $lineNumber as added and modified but not removed', - ({ originalContent, newContent, lineNumber }) => { - const diff = computeDiff(originalContent, newContent)[0]; - - expect(diff.added).toBe(true); - expect(diff.modified).toBe(true); - expect(diff.removed).toBeUndefined(); - expect(diff.lineNumber).toBe(lineNumber); - }, - ); - }); - - describe('added', () => { - it.each` - originalContent | newContent | lineNumber - ${'123'} | ${'123\n123'} | ${1} - ${'123\n123\n123'} | ${'123\n123\n1234\n123'} | ${3} - `( - 'marks line $lineNumber as added but not modified and not removed', - ({ originalContent, newContent, lineNumber }) => { - const diff = computeDiff(originalContent, newContent)[0]; - - expect(diff.added).toBe(true); - expect(diff.modified).toBeUndefined(); - expect(diff.removed).toBeUndefined(); - expect(diff.lineNumber).toBe(lineNumber); - }, - ); - }); - - describe('removed', () => { - it.each` - originalContent | newContent | lineNumber | modified - ${'123'} | ${''} | ${1} | ${undefined} - ${'123\n123\n123'} | ${'123\n123'} | ${2} | ${true} - `( - 'marks line $lineNumber as removed', - ({ originalContent, newContent, lineNumber, modified }) => { - const diff = computeDiff(originalContent, newContent)[0]; - - expect(diff.added).toBeUndefined(); - expect(diff.modified).toBe(modified); - expect(diff.removed).toBe(true); - expect(diff.lineNumber).toBe(lineNumber); - }, - ); - }); - - it('includes line number of change', () => { - const diff = computeDiff('123', '')[0]; - - expect(diff.lineNumber).toBe(1); - }); - - it('includes end line number of change', () => { - const diff = computeDiff('123', '')[0]; - - expect(diff.endLineNumber).toBe(1); - }); - - it('disregards changes for EOL type changes', () => { - const text1 = 'line1\nline2\nline3\n'; - const text2 = 'line1\r\nline2\r\nline3\r\n'; - - expect(computeDiff(text1, text2)).toEqual([]); - expect(computeDiff(text2, text1)).toEqual([]); - }); - }); -}); diff --git a/spec/frontend/ide/lib/editor_spec.js b/spec/frontend/ide/lib/editor_spec.js deleted file mode 100644 index c21a7edb2da..00000000000 --- a/spec/frontend/ide/lib/editor_spec.js +++ /dev/null @@ -1,344 +0,0 @@ -import { - editor as monacoEditor, - languages as monacoLanguages, - Range, - Selection, -} from 'monaco-editor'; -import { EDITOR_TYPE_DIFF } from '~/editor/constants'; -import Editor from '~/ide/lib/editor'; -import { defaultEditorOptions } from '~/ide/lib/editor_options'; -import { createStore } from '~/ide/stores'; -import { file } from '../helpers'; - -describe('Multi-file editor library', () => { - let instance; - let el; - let holder; - let store; - - const setNodeOffsetWidth = (val) => { - Object.defineProperty(instance.instance.getDomNode(), 'offsetWidth', { - get() { - return val; - }, - }); - }; - - beforeEach(() => { - store = createStore(); - el = document.createElement('div'); - holder = document.createElement('div'); - el.appendChild(holder); - - document.body.appendChild(el); - - instance = Editor.create(store); - }); - - afterEach(() => { - instance.modelManager.dispose(); - instance.dispose(); - Editor.editorInstance = null; - - el.remove(); - }); - - it('creates instance of editor', () => { - expect(Editor.editorInstance).not.toBeNull(); - }); - - it('creates instance returns cached instance', () => { - expect(Editor.create(store)).toEqual(instance); - }); - - describe('createInstance', () => { - it('creates editor instance', () => { - jest.spyOn(monacoEditor, 'create'); - - instance.createInstance(holder); - - expect(monacoEditor.create).toHaveBeenCalled(); - }); - - it('creates dirty diff controller', () => { - instance.createInstance(holder); - - expect(instance.dirtyDiffController).not.toBeNull(); - }); - - it('creates model manager', () => { - instance.createInstance(holder); - - expect(instance.modelManager).not.toBeNull(); - }); - }); - - describe('createDiffInstance', () => { - it('creates editor instance', () => { - jest.spyOn(monacoEditor, 'createDiffEditor'); - - instance.createDiffInstance(holder); - - expect(monacoEditor.createDiffEditor).toHaveBeenCalledWith(holder, { - ...defaultEditorOptions, - ignoreTrimWhitespace: false, - quickSuggestions: false, - occurrencesHighlight: false, - renderSideBySide: false, - readOnly: false, - renderLineHighlight: 'none', - hideCursorInOverviewRuler: true, - }); - }); - }); - - describe('createModel', () => { - it('calls model manager addModel', () => { - jest.spyOn(instance.modelManager, 'addModel').mockImplementation(() => {}); - - instance.createModel('FILE'); - - expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE', null); - }); - }); - - describe('attachModel', () => { - let model; - - beforeEach(() => { - instance.createInstance(document.createElement('div')); - - model = instance.createModel(file()); - }); - - it('sets the current model on the instance', () => { - instance.attachModel(model); - - expect(instance.currentModel).toBe(model); - }); - - it('attaches the model to the current instance', () => { - jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {}); - - instance.attachModel(model); - - expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel()); - }); - - it('sets original & modified when diff editor', () => { - jest.spyOn(instance.instance, 'getEditorType').mockReturnValue(EDITOR_TYPE_DIFF); - jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {}); - - instance.attachModel(model); - - expect(instance.instance.setModel).toHaveBeenCalledWith({ - original: model.getOriginalModel(), - modified: model.getModel(), - }); - }); - - it('attaches the model to the dirty diff controller', () => { - jest.spyOn(instance.dirtyDiffController, 'attachModel').mockImplementation(() => {}); - - instance.attachModel(model); - - expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model); - }); - - it('re-decorates with the dirty diff controller', () => { - jest.spyOn(instance.dirtyDiffController, 'reDecorate').mockImplementation(() => {}); - - instance.attachModel(model); - - expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model); - }); - }); - - describe('attachMergeRequestModel', () => { - let model; - - beforeEach(() => { - instance.createDiffInstance(document.createElement('div')); - - const f = file(); - f.mrChanges = { diff: 'ABC' }; - f.baseRaw = 'testing'; - - model = instance.createModel(f); - }); - - it('sets original & modified', () => { - jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {}); - - instance.attachMergeRequestModel(model); - - expect(instance.instance.setModel).toHaveBeenCalledWith({ - original: model.getBaseModel(), - modified: model.getModel(), - }); - }); - }); - - describe('clearEditor', () => { - it('resets the editor model', () => { - instance.createInstance(document.createElement('div')); - - jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {}); - - instance.clearEditor(); - - expect(instance.instance.setModel).toHaveBeenCalledWith(null); - }); - }); - - describe('languages', () => { - it('registers custom languages defined with Monaco', () => { - expect(monacoLanguages.getLanguages()).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: 'vue', - }), - ]), - ); - }); - }); - - describe('replaceSelectedText', () => { - let model; - let editor; - - beforeEach(() => { - instance.createInstance(holder); - - model = instance.createModel({ - ...file(), - key: 'index.md', - path: 'index.md', - }); - - instance.attachModel(model); - - editor = instance.instance; - editor.getModel().setValue('foo bar baz'); - editor.setSelection(new Range(1, 5, 1, 8)); - - instance.replaceSelectedText('hello'); - }); - - it('replaces the text selected in editor with the one provided', () => { - expect(editor.getModel().getValue()).toBe('foo hello baz'); - }); - - it('sets cursor to end of the replaced string', () => { - const selection = editor.getSelection(); - expect(selection).toEqual(new Selection(1, 10, 1, 10)); - }); - }); - - describe('dispose', () => { - it('calls disposble dispose method', () => { - jest.spyOn(instance.disposable, 'dispose'); - - instance.dispose(); - - expect(instance.disposable.dispose).toHaveBeenCalled(); - }); - - it('resets instance', () => { - instance.createInstance(document.createElement('div')); - - expect(instance.instance).not.toBeNull(); - - instance.dispose(); - - expect(instance.instance).toBeNull(); - }); - - it('does not dispose modelManager', () => { - jest.spyOn(instance.modelManager, 'dispose').mockImplementation(() => {}); - - instance.dispose(); - - expect(instance.modelManager.dispose).not.toHaveBeenCalled(); - }); - - it('does not dispose decorationsController', () => { - jest.spyOn(instance.decorationsController, 'dispose').mockImplementation(() => {}); - - instance.dispose(); - - expect(instance.decorationsController.dispose).not.toHaveBeenCalled(); - }); - }); - - describe('updateDiffView', () => { - describe('edit mode', () => { - it('does not update options', () => { - instance.createInstance(holder); - - jest.spyOn(instance.instance, 'updateOptions').mockImplementation(() => {}); - - instance.updateDiffView(); - - expect(instance.instance.updateOptions).not.toHaveBeenCalled(); - }); - }); - - describe('diff mode', () => { - beforeEach(() => { - instance.createDiffInstance(holder); - - jest.spyOn(instance.instance, 'updateOptions'); - }); - - it('sets renderSideBySide to false if el is less than 700 pixels', () => { - setNodeOffsetWidth(600); - - expect(instance.instance.updateOptions).not.toHaveBeenCalledWith({ - renderSideBySide: false, - }); - }); - - it('sets renderSideBySide to false if el is more than 700 pixels', () => { - setNodeOffsetWidth(800); - - expect(instance.instance.updateOptions).not.toHaveBeenCalledWith({ - renderSideBySide: true, - }); - }); - }); - }); - - describe('isDiffEditorType', () => { - it('returns true when diff editor', () => { - instance.createDiffInstance(holder); - - expect(instance.isDiffEditorType).toBe(true); - }); - - it('returns false when not diff editor', () => { - instance.createInstance(holder); - - expect(instance.isDiffEditorType).toBe(false); - }); - }); - - it('sets quickSuggestions to false when language is markdown', () => { - instance.createInstance(holder); - - jest.spyOn(instance.instance, 'updateOptions'); - - const model = instance.createModel({ - ...file(), - key: 'index.md', - path: 'index.md', - }); - - instance.attachModel(model); - - expect(instance.instance.updateOptions).toHaveBeenCalledWith({ - readOnly: false, - quickSuggestions: false, - }); - }); -}); diff --git a/spec/frontend/ide/lib/editorconfig/mock_data.js b/spec/frontend/ide/lib/editorconfig/mock_data.js deleted file mode 100644 index b21f4a5b735..00000000000 --- a/spec/frontend/ide/lib/editorconfig/mock_data.js +++ /dev/null @@ -1,146 +0,0 @@ -export const exampleConfigs = [ - { - path: 'foo/bar/baz/.editorconfig', - content: ` -[*] -tab_width = 6 -indent_style = tab -`, - }, - { - path: 'foo/bar/.editorconfig', - content: ` -root = false - -[*] -indent_size = 5 -indent_style = space -trim_trailing_whitespace = true - -[*_spec.{js,py}] -end_of_line = crlf - `, - }, - { - path: 'foo/.editorconfig', - content: ` -[*] -tab_width = 4 -indent_style = tab - `, - }, - { - path: '.editorconfig', - content: ` -root = true - -[*] -indent_size = 3 -indent_style = space -end_of_line = lf -insert_final_newline = true - -[*.js] -indent_size = 2 -indent_style = space -trim_trailing_whitespace = true - -[*.txt] -end_of_line = crlf - `, - }, - { - path: 'foo/bar/root/.editorconfig', - content: ` -root = true - -[*] -tab_width = 1 -indent_style = tab - `, - }, -]; - -export const exampleFiles = [ - { - path: 'foo/bar/root/README.md', - rules: { - indent_style: 'tab', // foo/bar/root/.editorconfig - tab_width: '1', // foo/bar/root/.editorconfig - }, - monacoRules: { - insertSpaces: false, - tabSize: 1, - }, - }, - { - path: 'foo/bar/baz/my_spec.js', - rules: { - end_of_line: 'crlf', // foo/bar/.editorconfig (for _spec.js files) - indent_size: '5', // foo/bar/.editorconfig - indent_style: 'tab', // foo/bar/baz/.editorconfig - insert_final_newline: 'true', // .editorconfig - tab_width: '6', // foo/bar/baz/.editorconfig - trim_trailing_whitespace: 'true', // .editorconfig (for .js files) - }, - monacoRules: { - endOfLine: 1, - insertFinalNewline: true, - insertSpaces: false, - tabSize: 6, - trimTrailingWhitespace: true, - }, - }, - { - path: 'foo/my_file.js', - rules: { - end_of_line: 'lf', // .editorconfig - indent_size: '2', // .editorconfig (for .js files) - indent_style: 'tab', // foo/.editorconfig - insert_final_newline: 'true', // .editorconfig - tab_width: '4', // foo/.editorconfig - trim_trailing_whitespace: 'true', // .editorconfig (for .js files) - }, - monacoRules: { - endOfLine: 0, - insertFinalNewline: true, - insertSpaces: false, - tabSize: 4, - trimTrailingWhitespace: true, - }, - }, - { - path: 'foo/my_file.md', - rules: { - end_of_line: 'lf', // .editorconfig - indent_size: '3', // .editorconfig - indent_style: 'tab', // foo/.editorconfig - insert_final_newline: 'true', // .editorconfig - tab_width: '4', // foo/.editorconfig - }, - monacoRules: { - endOfLine: 0, - insertFinalNewline: true, - insertSpaces: false, - tabSize: 4, - }, - }, - { - path: 'foo/bar/my_file.txt', - rules: { - end_of_line: 'crlf', // .editorconfig (for .txt files) - indent_size: '5', // foo/bar/.editorconfig - indent_style: 'space', // foo/bar/.editorconfig - insert_final_newline: 'true', // .editorconfig - tab_width: '4', // foo/.editorconfig - trim_trailing_whitespace: 'true', // foo/bar/.editorconfig - }, - monacoRules: { - endOfLine: 1, - insertFinalNewline: true, - insertSpaces: true, - tabSize: 4, - trimTrailingWhitespace: true, - }, - }, -]; diff --git a/spec/frontend/ide/lib/editorconfig/parser_spec.js b/spec/frontend/ide/lib/editorconfig/parser_spec.js deleted file mode 100644 index c2b4a8e6c49..00000000000 --- a/spec/frontend/ide/lib/editorconfig/parser_spec.js +++ /dev/null @@ -1,18 +0,0 @@ -import { getRulesWithTraversal } from '~/ide/lib/editorconfig/parser'; -import { exampleConfigs, exampleFiles } from './mock_data'; - -describe('~/ide/lib/editorconfig/parser', () => { - const getExampleConfigContent = (path) => - Promise.resolve(exampleConfigs.find((x) => x.path === path)?.content); - - describe('getRulesWithTraversal', () => { - it.each(exampleFiles)( - 'traverses through all editorconfig files in parent directories (until root=true is hit) and finds rules for this file (case %#)', - ({ path, rules }) => { - return getRulesWithTraversal(path, getExampleConfigContent).then((result) => { - expect(result).toEqual(rules); - }); - }, - ); - }); -}); diff --git a/spec/frontend/ide/lib/editorconfig/rules_mapper_spec.js b/spec/frontend/ide/lib/editorconfig/rules_mapper_spec.js deleted file mode 100644 index 536b1409435..00000000000 --- a/spec/frontend/ide/lib/editorconfig/rules_mapper_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import mapRulesToMonaco from '~/ide/lib/editorconfig/rules_mapper'; - -describe('mapRulesToMonaco', () => { - const multipleEntries = { - input: { indent_style: 'tab', indent_size: '4', insert_final_newline: 'true' }, - output: { insertSpaces: false, tabSize: 4, insertFinalNewline: true }, - }; - - // tab width takes precedence - const tabWidthAndIndent = { - input: { indent_style: 'tab', indent_size: '4', tab_width: '3' }, - output: { insertSpaces: false, tabSize: 3 }, - }; - - it.each` - rule | monacoOption - ${{ indent_style: 'tab' }} | ${{ insertSpaces: false }} - ${{ indent_style: 'space' }} | ${{ insertSpaces: true }} - ${{ indent_style: 'unset' }} | ${{}} - ${{ indent_size: '4' }} | ${{ tabSize: 4 }} - ${{ indent_size: '4.4' }} | ${{ tabSize: 4 }} - ${{ indent_size: '0' }} | ${{}} - ${{ indent_size: '-10' }} | ${{}} - ${{ indent_size: 'NaN' }} | ${{}} - ${{ tab_width: '4' }} | ${{ tabSize: 4 }} - ${{ tab_width: '5.4' }} | ${{ tabSize: 5 }} - ${{ tab_width: '-10' }} | ${{}} - ${{ trim_trailing_whitespace: 'true' }} | ${{ trimTrailingWhitespace: true }} - ${{ trim_trailing_whitespace: 'false' }} | ${{ trimTrailingWhitespace: false }} - ${{ trim_trailing_whitespace: 'unset' }} | ${{}} - ${{ end_of_line: 'lf' }} | ${{ endOfLine: 0 }} - ${{ end_of_line: 'crlf' }} | ${{ endOfLine: 1 }} - ${{ end_of_line: 'cr' }} | ${{}} - ${{ end_of_line: 'unset' }} | ${{}} - ${{ insert_final_newline: 'true' }} | ${{ insertFinalNewline: true }} - ${{ insert_final_newline: 'false' }} | ${{ insertFinalNewline: false }} - ${{ insert_final_newline: 'unset' }} | ${{}} - ${multipleEntries.input} | ${multipleEntries.output} - ${tabWidthAndIndent.input} | ${tabWidthAndIndent.output} - `('correctly maps editorconfig rule to monaco option: $rule', ({ rule, monacoOption }) => { - expect(mapRulesToMonaco(rule)).toEqual(monacoOption); - }); -}); diff --git a/spec/frontend/ide/lib/errors_spec.js b/spec/frontend/ide/lib/errors_spec.js deleted file mode 100644 index 2e4acdb8a63..00000000000 --- a/spec/frontend/ide/lib/errors_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import { - createUnexpectedCommitError, - createCodeownersCommitError, - createBranchChangedCommitError, - branchAlreadyExistsCommitError, - parseCommitError, -} from '~/ide/lib/errors'; - -const TEST_SPECIAL = '&special<'; -const TEST_SPECIAL_ESCAPED = '&special<'; -const TEST_MESSAGE = 'Test message.'; -const CODEOWNERS_MESSAGE = - 'Push to protected branches that contain changes to files matching CODEOWNERS is not allowed'; -const CHANGED_MESSAGE = 'Things changed since you started editing'; - -describe('~/ide/lib/errors', () => { - const createResponseError = (message) => ({ - response: { - data: { - message, - }, - }, - }); - - const NEW_BRANCH_SUFFIX = `

    Would you like to create a new branch?`; - const AUTOGENERATE_SUFFIX = `

    Would you like to try auto-generating a branch name?`; - - it.each` - fn | title | message | messageHTML - ${createCodeownersCommitError} | ${'CODEOWNERS rule violation'} | ${TEST_MESSAGE} | ${TEST_MESSAGE} - ${createCodeownersCommitError} | ${'CODEOWNERS rule violation'} | ${TEST_SPECIAL} | ${TEST_SPECIAL_ESCAPED} - ${branchAlreadyExistsCommitError} | ${'Branch already exists'} | ${TEST_MESSAGE} | ${`${TEST_MESSAGE}${AUTOGENERATE_SUFFIX}`} - ${branchAlreadyExistsCommitError} | ${'Branch already exists'} | ${TEST_SPECIAL} | ${`${TEST_SPECIAL_ESCAPED}${AUTOGENERATE_SUFFIX}`} - ${createBranchChangedCommitError} | ${'Branch changed'} | ${TEST_MESSAGE} | ${`${TEST_MESSAGE}${NEW_BRANCH_SUFFIX}`} - ${createBranchChangedCommitError} | ${'Branch changed'} | ${TEST_SPECIAL} | ${`${TEST_SPECIAL_ESCAPED}${NEW_BRANCH_SUFFIX}`} - `('$fn escapes and uses given message="$message"', ({ fn, title, message, messageHTML }) => { - expect(fn(message)).toEqual({ - title, - messageHTML, - primaryAction: { text: 'Create new branch', callback: expect.any(Function) }, - }); - }); - - describe('parseCommitError', () => { - it.each` - message | expectation - ${null} | ${createUnexpectedCommitError()} - ${{}} | ${createUnexpectedCommitError()} - ${{ response: {} }} | ${createUnexpectedCommitError()} - ${{ response: { data: {} } }} | ${createUnexpectedCommitError()} - ${createResponseError(TEST_MESSAGE)} | ${createUnexpectedCommitError(TEST_MESSAGE)} - ${createResponseError(CODEOWNERS_MESSAGE)} | ${createCodeownersCommitError(CODEOWNERS_MESSAGE)} - ${createResponseError(CHANGED_MESSAGE)} | ${createBranchChangedCommitError(CHANGED_MESSAGE)} - `('parses message into error object with "$message"', ({ message, expectation }) => { - expect(parseCommitError(message)).toEqual(expectation); - }); - }); -}); diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js deleted file mode 100644 index 50738af0e33..00000000000 --- a/spec/frontend/ide/lib/files_spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import { decorateFiles, splitParent } from '~/ide/lib/files'; -import { decorateData } from '~/ide/stores/utils'; - -const TEST_BLOB_DATA = { mimeType: 'test/mime' }; - -const createEntries = (paths) => { - const createEntry = (acc, { path, type, children }) => { - const { name, parent } = splitParent(path); - - acc[path] = { - ...decorateData({ - id: path, - name, - path, - type, - parentPath: parent, - }), - tree: children.map((childName) => expect.objectContaining({ name: childName })), - ...(type === 'blob' ? TEST_BLOB_DATA : {}), - }; - - return acc; - }; - - const entries = paths.reduce(createEntry, {}); - - // Wrap entries in expect.objectContaining. - // We couldn't do this earlier because we still need to select properties from parent entries. - return Object.keys(entries).reduce((acc, key) => { - acc[key] = expect.objectContaining(entries[key]); - - return acc; - }, {}); -}; - -describe('IDE lib decorate files', () => { - it('creates entries and treeList', () => { - const data = ['app/assets/apples/foo.js', 'app/bugs.js', 'app/#weird#file?.txt', 'README.md']; - const expectedEntries = createEntries([ - { path: 'app', type: 'tree', children: ['assets', '#weird#file?.txt', 'bugs.js'] }, - { path: 'app/assets', type: 'tree', children: ['apples'] }, - { path: 'app/assets/apples', type: 'tree', children: ['foo.js'] }, - { path: 'app/assets/apples/foo.js', type: 'blob', children: [] }, - { path: 'app/bugs.js', type: 'blob', children: [] }, - { path: 'app/#weird#file?.txt', type: 'blob', children: [] }, - { path: 'README.md', type: 'blob', children: [] }, - ]); - - const { entries, treeList } = decorateFiles({ data, blobData: TEST_BLOB_DATA }); - - // Here we test the keys and then each key/value individually because `expect(entries).toEqual(expectedEntries)` - // was taking a very long time for some reason. Probably due to large objects and nested `expect.objectContaining`. - const entryKeys = Object.keys(entries); - - expect(entryKeys).toEqual(Object.keys(expectedEntries)); - entryKeys.forEach((key) => { - expect(entries[key]).toEqual(expectedEntries[key]); - }); - - expect(treeList).toEqual([expectedEntries.app, expectedEntries['README.md']]); - }); -}); diff --git a/spec/frontend/ide/lib/mirror_spec.js b/spec/frontend/ide/lib/mirror_spec.js deleted file mode 100644 index 98e6b0deee6..00000000000 --- a/spec/frontend/ide/lib/mirror_spec.js +++ /dev/null @@ -1,188 +0,0 @@ -import createDiff from '~/ide/lib/create_diff'; -import { - canConnect, - createMirror, - SERVICE_NAME, - PROTOCOL, - MSG_CONNECTION_ERROR, - SERVICE_DELAY, -} from '~/ide/lib/mirror'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { getWebSocketUrl } from '~/lib/utils/url_utility'; - -jest.mock('~/ide/lib/create_diff', () => jest.fn()); - -const TEST_PATH = '/project/ide/proxy/path'; -const TEST_DIFF = { - patch: 'lorem ipsum', - toDelete: ['foo.md'], -}; -const TEST_ERROR = 'Something bad happened...'; -const TEST_SUCCESS_RESPONSE = { - data: JSON.stringify({ error: { code: 0 }, payload: { status_code: HTTP_STATUS_OK } }), -}; -const TEST_ERROR_RESPONSE = { - data: JSON.stringify({ - error: { code: 1, Message: TEST_ERROR }, - payload: { status_code: HTTP_STATUS_OK }, - }), -}; -const TEST_ERROR_PAYLOAD_RESPONSE = { - data: JSON.stringify({ - error: { code: 0 }, - payload: { status_code: HTTP_STATUS_INTERNAL_SERVER_ERROR, error_message: TEST_ERROR }, - }), -}; - -const buildUploadMessage = ({ toDelete, patch }) => - JSON.stringify({ - code: 'EVENT', - namespace: '/files', - event: 'PATCH', - payload: { diff: patch, delete_files: toDelete }, - }); - -describe('ide/lib/mirror', () => { - describe('canConnect', () => { - it('can connect if the session has the expected service', () => { - const result = canConnect({ services: ['test1', SERVICE_NAME, 'test2'] }); - - expect(result).toBe(true); - }); - - it('cannot connect if the session does not have the expected service', () => { - const result = canConnect({ services: ['test1', 'test2'] }); - - expect(result).toBe(false); - }); - }); - - describe('createMirror', () => { - const origWebSocket = global.WebSocket; - let mirror; - let mockWebSocket; - - beforeEach(() => { - mockWebSocket = { - close: jest.fn(), - send: jest.fn(), - }; - global.WebSocket = jest.fn().mockImplementation(() => mockWebSocket); - mirror = createMirror(); - }); - - afterEach(() => { - global.WebSocket = origWebSocket; - }); - - const waitForConnection = (delay = SERVICE_DELAY) => { - const wait = new Promise((resolve) => { - setTimeout(resolve, 10); - }); - - jest.advanceTimersByTime(delay); - - return wait; - }; - const connectPass = () => waitForConnection().then(() => mockWebSocket.onopen()); - const connectFail = () => waitForConnection().then(() => mockWebSocket.onerror()); - const sendResponse = (msg) => { - mockWebSocket.onmessage(msg); - }; - - describe('connect', () => { - let connection; - - beforeEach(() => { - connection = mirror.connect(TEST_PATH); - }); - - it('waits before creating web socket', () => { - // ignore error when test suite terminates - connection.catch(() => {}); - - return waitForConnection(SERVICE_DELAY - 10).then(() => { - expect(global.WebSocket).not.toHaveBeenCalled(); - }); - }); - - it('is canceled when disconnected before finished waiting', () => { - mirror.disconnect(); - - return waitForConnection(SERVICE_DELAY).then(() => { - expect(global.WebSocket).not.toHaveBeenCalled(); - }); - }); - - describe('when connection is successful', () => { - beforeEach(connectPass); - - it('connects to service', () => { - const expectedPath = `${getWebSocketUrl(TEST_PATH)}?service=${SERVICE_NAME}`; - - return connection.then(() => { - expect(global.WebSocket).toHaveBeenCalledWith(expectedPath, [PROTOCOL]); - }); - }); - - it('disconnects when connected again', () => { - const result = connection - .then(() => { - // https://gitlab.com/gitlab-org/gitlab/issues/33024 - // eslint-disable-next-line promise/no-nesting - mirror.connect(TEST_PATH).catch(() => {}); - }) - .then(() => { - expect(mockWebSocket.close).toHaveBeenCalled(); - }); - - return result; - }); - }); - - describe('when connection fails', () => { - beforeEach(connectFail); - - it('rejects with error', () => { - return expect(connection).rejects.toEqual(new Error(MSG_CONNECTION_ERROR)); - }); - }); - }); - - describe('upload', () => { - let state; - - beforeEach(() => { - state = { changedFiles: [] }; - createDiff.mockReturnValue(TEST_DIFF); - - const connection = mirror.connect(TEST_PATH); - - return connectPass().then(() => connection); - }); - - it('creates a diff from the given state', () => { - const result = mirror.upload(state); - - sendResponse(TEST_SUCCESS_RESPONSE); - - return result.then(() => { - expect(createDiff).toHaveBeenCalledWith(state); - expect(mockWebSocket.send).toHaveBeenCalledWith(buildUploadMessage(TEST_DIFF)); - }); - }); - - it.each` - response | description - ${TEST_ERROR_RESPONSE} | ${'error in error'} - ${TEST_ERROR_PAYLOAD_RESPONSE} | ${'error in payload'} - `('rejects if response has $description', ({ response }) => { - const result = mirror.upload(state); - - sendResponse(response); - - return expect(result).rejects.toEqual({ message: TEST_ERROR }); - }); - }); - }); -}); diff --git a/spec/frontend/ide/mock_data.js b/spec/frontend/ide/mock_data.js deleted file mode 100644 index 722f15db87d..00000000000 --- a/spec/frontend/ide/mock_data.js +++ /dev/null @@ -1,237 +0,0 @@ -import { TEST_HOST } from 'spec/test_constants'; - -export const projectData = { - id: 1, - name: 'abcproject', - web_url: '', - avatar_url: '', - path: '', - name_with_namespace: 'namespace/abcproject', - branches: { - main: { - treeId: 'abcproject/main', - can_push: true, - commit: { - id: '123', - short_id: 'abc123de', - committed_date: '2019-09-13T15:37:30+0300', - }, - }, - }, - mergeRequests: {}, - merge_requests_enabled: true, - userPermissions: {}, - default_branch: 'main', -}; - -export const pipelines = [ - { - id: 1, - ref: 'main', - sha: '123', - details: { - status: { - icon: 'status_failed', - group: 'failed', - text: 'Failed', - }, - }, - commit: { id: '123' }, - }, - { - id: 2, - ref: 'main', - sha: '213', - details: { - status: { - icon: 'status_failed', - group: 'failed', - text: 'Failed', - }, - }, - commit: { id: '213' }, - }, -]; - -export const stages = [ - { - dropdown_path: `${TEST_HOST}/testing`, - name: 'build', - status: { - icon: 'status_failed', - group: 'failed', - text: 'failed', - }, - }, - { - dropdown_path: 'testing', - name: 'test', - status: { - icon: 'status_failed', - group: 'failed', - text: 'failed', - }, - }, -]; - -export const jobs = [ - { - id: 1, - name: 'test', - path: 'testing', - status: { - icon: 'status_success', - group: 'success', - text: 'passed', - }, - stage: 'test', - duration: 1, - started: new Date(), - }, - { - id: 2, - name: 'test 2', - path: 'testing2', - status: { - icon: 'status_success', - text: 'passed', - }, - stage: 'test', - duration: 1, - started: new Date(), - }, - { - id: 3, - name: 'test 3', - path: 'testing3', - status: { - icon: 'status_success', - text: 'passed', - }, - stage: 'test', - duration: 1, - started: new Date(), - }, - { - id: 4, - name: 'test 4', - // bridge jobs don't have details page and so there is no path attribute - // see https://gitlab.com/gitlab-org/gitlab/-/issues/216480 - status: { - icon: 'status_failed', - text: 'failed', - }, - stage: 'build', - duration: 1, - started: new Date(), - }, -]; - -export const fullPipelinesResponse = { - data: { - count: { - all: 2, - }, - pipelines: [ - { - id: '51', - path: 'test', - commit: { - id: '123', - }, - details: { - status: { - icon: 'status_failed', - text: 'failed', - }, - stages: [...stages], - }, - }, - { - id: '50', - commit: { - id: 'abc123def456ghi789jkl', - }, - details: { - status: { - icon: 'status_success', - text: 'passed', - }, - stages: [...stages], - }, - }, - ], - }, -}; - -export const mergeRequests = [ - { - id: 1, - iid: 1, - title: 'Test merge request', - project_id: 1, - web_url: `${TEST_HOST}/namespace/project-path/-/merge_requests/1`, - references: { - short: '!1', - full: 'namespace/project-path!1', - }, - }, -]; - -export const branches = [ - { - id: 1, - name: 'main', - commit: { - message: 'Update main branch', - committed_date: '2018-08-01T00:20:05Z', - }, - can_push: true, - protected: true, - default: true, - }, - { - id: 2, - name: 'protected/no-access', - commit: { - message: 'Update some stuff', - committed_date: '2018-08-02T00:00:05Z', - }, - can_push: false, - protected: true, - default: false, - }, - { - id: 3, - name: 'protected/access', - commit: { - message: 'Update some stuff', - committed_date: '2018-08-02T00:00:05Z', - }, - can_push: true, - protected: true, - default: false, - }, - { - id: 4, - name: 'regular', - commit: { - message: 'Update some more stuff', - committed_date: '2018-06-30T00:20:05Z', - }, - can_push: true, - protected: false, - default: false, - }, - { - id: 5, - name: 'regular/no-access', - commit: { - message: 'Update some more stuff', - committed_date: '2018-06-30T00:20:05Z', - }, - can_push: false, - protected: false, - default: false, - }, -]; diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js deleted file mode 100644 index 45a3cc6adf0..00000000000 --- a/spec/frontend/ide/services/index_spec.js +++ /dev/null @@ -1,284 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql'; -import Api from '~/api'; -import services from '~/ide/services'; -import { query } from '~/ide/services/gql'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { escapeFileUrl } from '~/lib/utils/url_utility'; -import { projectData } from '../mock_data'; - -jest.mock('~/api'); -jest.mock('~/ide/services/gql'); - -const TEST_NAMESPACE = 'alice'; -const TEST_PROJECT = 'wonderland'; -const TEST_PROJECT_ID = `${TEST_NAMESPACE}/${TEST_PROJECT}`; -const TEST_BRANCH = 'main-patch-123'; -const TEST_COMMIT_SHA = '123456789'; -const TEST_FILE_PATH = 'README2.md'; -const TEST_FILE_OLD_PATH = 'OLD_README2.md'; -const TEST_FILE_PATH_SPECIAL = 'READM?ME/abc'; -const TEST_FILE_CONTENTS = 'raw file content'; - -describe('IDE services', () => { - describe('commit', () => { - let payload; - - beforeEach(() => { - payload = { - branch: TEST_BRANCH, - commit_message: 'Hello world', - actions: [], - start_sha: TEST_COMMIT_SHA, - }; - - Api.commitMultiple.mockReturnValue(Promise.resolve()); - }); - - it('should commit', () => { - services.commit(TEST_PROJECT_ID, payload); - - expect(Api.commitMultiple).toHaveBeenCalledWith(TEST_PROJECT_ID, payload); - }); - }); - - describe('getRawFileData', () => { - it("resolves with a file's content if its a tempfile and it isn't renamed", () => { - const file = { - path: 'file', - tempFile: true, - content: 'content', - raw: 'raw content', - }; - - return services.getRawFileData(file).then((raw) => { - expect(raw).toBe('content'); - }); - }); - - it('resolves with file.raw if the file is renamed', () => { - const file = { - path: 'file', - tempFile: true, - content: 'content', - prevPath: 'old_path', - raw: 'raw content', - }; - - return services.getRawFileData(file).then((raw) => { - expect(raw).toBe('raw content'); - }); - }); - - it('returns file.raw if it exists', () => { - const file = { - path: 'file', - content: 'content', - raw: 'raw content', - }; - - return services.getRawFileData(file).then((raw) => { - expect(raw).toBe('raw content'); - }); - }); - - it("returns file.raw if file.raw is empty but file.rawPath doesn't exist", () => { - const file = { - path: 'file', - content: 'content', - raw: '', - }; - - return services.getRawFileData(file).then((raw) => { - expect(raw).toBe(''); - }); - }); - - describe("if file.rawPath exists but file.raw doesn't exist", () => { - let file; - let mock; - beforeEach(() => { - file = { - path: 'file', - content: 'content', - raw: '', - rawPath: 'some_raw_path', - }; - - mock = new MockAdapter(axios); - mock.onGet(file.rawPath).reply(HTTP_STATUS_OK, 'raw content'); - - jest.spyOn(axios, 'get'); - }); - - afterEach(() => { - mock.restore(); - }); - - it('sends a request to file.rawPath', () => { - return services.getRawFileData(file).then((raw) => { - expect(axios.get).toHaveBeenCalledWith(file.rawPath, { - transformResponse: [expect.any(Function)], - }); - expect(raw).toEqual('raw content'); - }); - }); - - it('returns arraybuffer for binary files', () => { - file.binary = true; - - return services.getRawFileData(file).then((raw) => { - expect(axios.get).toHaveBeenCalledWith(file.rawPath, { - transformResponse: [expect.any(Function)], - responseType: 'arraybuffer', - }); - expect(raw).toEqual('raw content'); - }); - }); - }); - }); - - describe('getBaseRawFileData', () => { - let file; - let mock; - - beforeEach(() => { - file = { - mrChange: null, - projectId: TEST_PROJECT_ID, - path: TEST_FILE_PATH, - }; - - jest.spyOn(axios, 'get'); - - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.restore(); - }); - - it('gives back file.baseRaw for files with that property present', () => { - file.baseRaw = TEST_FILE_CONTENTS; - - return services.getBaseRawFileData(file, TEST_PROJECT_ID, TEST_COMMIT_SHA).then((content) => { - expect(content).toEqual(TEST_FILE_CONTENTS); - }); - }); - - it('gives back file.baseRaw for files for temp files', () => { - file.tempFile = true; - file.baseRaw = TEST_FILE_CONTENTS; - - return services.getBaseRawFileData(file, TEST_PROJECT_ID, TEST_COMMIT_SHA).then((content) => { - expect(content).toEqual(TEST_FILE_CONTENTS); - }); - }); - - describe.each` - relativeUrlRoot | filePath | isRenamed - ${''} | ${TEST_FILE_PATH} | ${false} - ${''} | ${TEST_FILE_OLD_PATH} | ${true} - ${''} | ${TEST_FILE_PATH_SPECIAL} | ${false} - ${''} | ${TEST_FILE_PATH_SPECIAL} | ${true} - ${'gitlab'} | ${TEST_FILE_OLD_PATH} | ${true} - `( - 'with relativeUrlRoot ($relativeUrlRoot) and filePath ($filePath) and isRenamed ($isRenamed)', - ({ relativeUrlRoot, filePath, isRenamed }) => { - beforeEach(() => { - if (isRenamed) { - file.mrChange = { - renamed_file: true, - old_path: filePath, - }; - } else { - file.path = filePath; - } - - gon.relative_url_root = relativeUrlRoot; - - mock - .onGet( - `${relativeUrlRoot}/${TEST_PROJECT_ID}/-/raw/${TEST_COMMIT_SHA}/${escapeFileUrl( - filePath, - )}`, - ) - .reply(HTTP_STATUS_OK, TEST_FILE_CONTENTS); - }); - - it('fetches file content', () => - services.getBaseRawFileData(file, TEST_PROJECT_ID, TEST_COMMIT_SHA).then((content) => { - expect(content).toEqual(TEST_FILE_CONTENTS); - })); - }, - ); - }); - - describe('getFiles', () => { - let mock; - let relativeUrlRoot; - const TEST_RELATIVE_URL_ROOT = 'blah-blah'; - - beforeEach(() => { - jest.spyOn(axios, 'get'); - relativeUrlRoot = gon.relative_url_root; - gon.relative_url_root = TEST_RELATIVE_URL_ROOT; - - mock = new MockAdapter(axios); - - mock - .onGet(`${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`) - .reply(HTTP_STATUS_OK, [TEST_FILE_PATH]); - }); - - afterEach(() => { - mock.restore(); - gon.relative_url_root = relativeUrlRoot; - }); - - it('initates the api call based on the passed path and commit hash', () => { - return services.getFiles(TEST_PROJECT_ID, TEST_COMMIT_SHA).then(({ data }) => { - expect(axios.get).toHaveBeenCalledWith( - `${gon.relative_url_root}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`, - expect.any(Object), - ); - expect(data).toEqual([TEST_FILE_PATH]); - }); - }); - }); - - describe('getProjectPermissionsData', () => { - const TEST_PROJECT_PATH = 'foo/bar'; - - it('queries for the project permissions', () => { - const result = { data: { project: projectData } }; - query.mockResolvedValue(result); - - return services.getProjectPermissionsData(TEST_PROJECT_PATH).then((data) => { - expect(data).toEqual(result.data.project); - expect(query).toHaveBeenCalledWith( - expect.objectContaining({ - query: getIdeProject, - variables: { projectPath: TEST_PROJECT_PATH }, - }), - ); - }); - }); - - it('converts the returned GraphQL id to the regular ID number', () => { - const projectId = 2; - const gqlProjectData = { - id: `gid://gitlab/Project/${projectId}`, - userPermissions: { - bogus: true, - }, - }; - - query.mockResolvedValue({ data: { project: gqlProjectData } }); - return services.getProjectPermissionsData(TEST_PROJECT_PATH).then((data) => { - expect(data.id).toBe(projectId); - }); - }); - }); -}); diff --git a/spec/frontend/ide/services/terminals_spec.js b/spec/frontend/ide/services/terminals_spec.js deleted file mode 100644 index 5b6b60a250c..00000000000 --- a/spec/frontend/ide/services/terminals_spec.js +++ /dev/null @@ -1,50 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import * as terminalService from '~/ide/services/terminals'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; - -const TEST_PROJECT_PATH = 'lorem/ipsum/dolar'; -const TEST_BRANCH = 'ref'; - -describe('~/ide/services/terminals', () => { - let axiosSpy; - let mock; - - beforeEach(() => { - axiosSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, {}]); - - mock = new MockAdapter(axios); - mock.onPost(/.*/).reply((...args) => axiosSpy(...args)); - }); - - afterEach(() => { - mock.restore(); - }); - - it.each` - method | relativeUrlRoot | url - ${'checkConfig'} | ${''} | ${`/${TEST_PROJECT_PATH}/ide_terminals/check_config`} - ${'checkConfig'} | ${'/'} | ${`/${TEST_PROJECT_PATH}/ide_terminals/check_config`} - ${'checkConfig'} | ${'/gitlabbin'} | ${`/gitlabbin/${TEST_PROJECT_PATH}/ide_terminals/check_config`} - ${'create'} | ${''} | ${`/${TEST_PROJECT_PATH}/ide_terminals`} - ${'create'} | ${'/'} | ${`/${TEST_PROJECT_PATH}/ide_terminals`} - ${'create'} | ${'/gitlabbin'} | ${`/gitlabbin/${TEST_PROJECT_PATH}/ide_terminals`} - `( - 'when $method called, posts request to $url (relative_url_root=$relativeUrlRoot)', - async ({ method, url, relativeUrlRoot }) => { - gon.relative_url_root = relativeUrlRoot; - - await terminalService[method](TEST_PROJECT_PATH, TEST_BRANCH); - - expect(axiosSpy).toHaveBeenCalledWith( - expect.objectContaining({ - data: JSON.stringify({ - branch: TEST_BRANCH, - format: 'json', - }), - url, - }), - ); - }, - ); -}); diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js deleted file mode 100644 index 7f4e1cf761d..00000000000 --- a/spec/frontend/ide/stores/actions/file_spec.js +++ /dev/null @@ -1,830 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { nextTick } from 'vue'; -import eventHub from '~/ide/eventhub'; -import { createRouter } from '~/ide/ide_router'; -import service from '~/ide/services'; -import { createStore } from '~/ide/stores'; -import * as actions from '~/ide/stores/actions/file'; -import * as types from '~/ide/stores/mutation_types'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import { file, createTriggerRenameAction, createTriggerUpdatePayload } from '../../helpers'; - -const ORIGINAL_CONTENT = 'original content'; -const RELATIVE_URL_ROOT = '/gitlab'; - -describe('IDE store file actions', () => { - let mock; - let store; - let router; - - beforeEach(() => { - stubPerformanceWebAPI(); - - mock = new MockAdapter(axios); - window.gon = { - relative_url_root: RELATIVE_URL_ROOT, - }; - - store = createStore(); - - store.state.currentProjectId = 'test/test'; - store.state.currentBranchId = 'main'; - - router = createRouter(store); - - jest.spyOn(store, 'commit'); - jest.spyOn(store, 'dispatch'); - jest.spyOn(router, 'push').mockImplementation(() => {}); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('closeFile', () => { - let localFile; - - beforeEach(() => { - localFile = file('testFile'); - localFile.active = true; - localFile.opened = true; - - store.state.openFiles.push(localFile); - store.state.entries[localFile.path] = localFile; - }); - - it('closes open files', () => { - return store.dispatch('closeFile', localFile).then(() => { - expect(localFile.opened).toBe(false); - expect(localFile.active).toBe(false); - expect(store.state.openFiles.length).toBe(0); - }); - }); - - it('closes file even if file has changes', () => { - store.state.changedFiles.push(localFile); - - return store - .dispatch('closeFile', localFile) - .then(nextTick) - .then(() => { - expect(store.state.openFiles.length).toBe(0); - expect(store.state.changedFiles.length).toBe(1); - }); - }); - - it('switches to the next available file before closing the current one', () => { - const f = file('newOpenFile'); - - store.state.openFiles.push(f); - store.state.entries[f.path] = f; - - return store - .dispatch('closeFile', localFile) - .then(nextTick) - .then(() => { - expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/main/-/newOpenFile/'); - }); - }); - - it('removes file if it pending', () => { - store.state.openFiles = [ - { - ...localFile, - pending: true, - }, - ]; - - return store.dispatch('closeFile', localFile).then(() => { - expect(store.state.openFiles.length).toBe(0); - }); - }); - }); - - describe('setFileActive', () => { - let localFile; - let scrollToTabSpy; - let oldScrollToTab; - - beforeEach(() => { - scrollToTabSpy = jest.fn(); - oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line - store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line - - localFile = file('setThisActive'); - - store.state.entries[localFile.path] = localFile; - }); - - afterEach(() => { - store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line - }); - - it('calls scrollToTab', () => { - const dispatch = jest.fn(); - - actions.setFileActive( - { commit() {}, state: store.state, getters: store.getters, dispatch }, - localFile.path, - ); - - expect(dispatch).toHaveBeenCalledWith('scrollToTab'); - }); - - it('commits SET_FILE_ACTIVE', () => { - const commit = jest.fn(); - - actions.setFileActive( - { commit, state: store.state, getters: store.getters, dispatch() {} }, - localFile.path, - ); - - expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', { - path: localFile.path, - active: true, - }); - }); - - it('sets current active file to not active', () => { - const f = file('newActive'); - store.state.entries[f.path] = f; - localFile.active = true; - store.state.openFiles.push(localFile); - - const commit = jest.fn(); - - actions.setFileActive( - { commit, state: store.state, getters: store.getters, dispatch() {} }, - f.path, - ); - - expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', { - path: localFile.path, - active: false, - }); - }); - }); - - describe('getFileData', () => { - let localFile; - - beforeEach(() => { - jest.spyOn(service, 'getFileData'); - - localFile = file(`newCreate-${Math.random()}`); - store.state.entries[localFile.path] = localFile; - - store.state.currentProjectId = 'test/test'; - store.state.currentBranchId = 'main'; - - store.state.projects['test/test'] = { - branches: { - main: { - commit: { - id: '7297abc', - }, - }, - }, - }; - }); - - describe('call to service', () => { - const callExpectation = (serviceCalled) => { - store.dispatch('getFileData', { path: localFile.path }); - - if (serviceCalled) { - expect(service.getFileData).toHaveBeenCalled(); - } else { - expect(service.getFileData).not.toHaveBeenCalled(); - } - }; - - beforeEach(() => { - service.getFileData.mockImplementation(() => new Promise(() => {})); - }); - - it("isn't called if file.raw exists", () => { - localFile.raw = 'raw data'; - - callExpectation(false); - }); - - it("isn't called if file is a tempFile", () => { - localFile.raw = ''; - localFile.tempFile = true; - - callExpectation(false); - }); - - it('is called if file is a tempFile but also renamed', () => { - localFile.raw = ''; - localFile.tempFile = true; - localFile.prevPath = 'old_path'; - - callExpectation(true); - }); - - it('is called if tempFile but file was deleted and readded', () => { - localFile.raw = ''; - localFile.tempFile = true; - localFile.prevPath = 'old_path'; - - store.state.stagedFiles = [{ ...localFile, deleted: true }]; - - callExpectation(true); - }); - }); - - describe('success', () => { - beforeEach(() => { - mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/${localFile.path}`).replyOnce( - HTTP_STATUS_OK, - { - raw_path: 'raw_path', - }, - { - 'page-title': 'testing getFileData', - }, - ); - }); - - it('calls the service', () => { - return store.dispatch('getFileData', { path: localFile.path }).then(() => { - expect(service.getFileData).toHaveBeenCalledWith( - `${RELATIVE_URL_ROOT}/test/test/-/7297abc/${localFile.path}`, - ); - }); - }); - - it('sets document title with the branchId', () => { - return store.dispatch('getFileData', { path: localFile.path }).then(() => { - expect(document.title).toBe(`${localFile.path} · main · test/test · GitLab`); - }); - }); - - it('sets the file as active', () => { - return store.dispatch('getFileData', { path: localFile.path }).then(() => { - expect(localFile.active).toBe(true); - }); - }); - - it('sets the file not as active if we pass makeFileActive false', () => { - return store - .dispatch('getFileData', { path: localFile.path, makeFileActive: false }) - .then(() => { - expect(localFile.active).toBe(false); - }); - }); - - it('does not update the page title with the path of the file if makeFileActive is false', () => { - document.title = 'dummy title'; - return store - .dispatch('getFileData', { path: localFile.path, makeFileActive: false }) - .then(() => { - expect(document.title).toBe(`dummy title`); - }); - }); - - it('adds the file to open files', () => { - return store.dispatch('getFileData', { path: localFile.path }).then(() => { - expect(store.state.openFiles.length).toBe(1); - expect(store.state.openFiles[0].name).toBe(localFile.name); - }); - }); - - it('does not toggle loading if toggleLoading=false', () => { - expect(localFile.loading).toBe(false); - - return store - .dispatch('getFileData', { - path: localFile.path, - makeFileActive: false, - toggleLoading: false, - }) - .then(() => { - expect(localFile.loading).toBe(true); - }); - }); - }); - - describe('Re-named success', () => { - beforeEach(() => { - localFile = file(`newCreate-${Math.random()}`); - localFile.prevPath = 'old-dull-file'; - localFile.path = 'new-shiny-file'; - store.state.entries[localFile.path] = localFile; - - mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/old-dull-file`).replyOnce( - HTTP_STATUS_OK, - { - raw_path: 'raw_path', - }, - { - 'page-title': 'testing old-dull-file', - }, - ); - }); - - it('sets document title considering `prevPath` on a file', () => { - return store.dispatch('getFileData', { path: localFile.path }).then(() => { - expect(document.title).toBe(`new-shiny-file · main · test/test · GitLab`); - }); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/${localFile.path}`).networkError(); - }); - - it('dispatches error action', () => { - const dispatch = jest.fn(); - - return actions - .getFileData( - { state: store.state, commit() {}, dispatch, getters: store.getters }, - { path: localFile.path }, - ) - .then(() => { - expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occurred while loading the file.', - action: expect.any(Function), - actionText: 'Please try again', - actionPayload: { - path: localFile.path, - makeFileActive: true, - }, - }); - }); - }); - }); - }); - - describe('getRawFileData', () => { - let tmpFile; - - beforeEach(() => { - jest.spyOn(service, 'getRawFileData'); - - tmpFile = { ...file('tmpFile'), rawPath: 'raw_path' }; - store.state.entries[tmpFile.path] = tmpFile; - }); - - describe('success', () => { - beforeEach(() => { - mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_OK, 'raw'); - }); - - it('calls getRawFileData service method', () => { - return store.dispatch('getRawFileData', { path: tmpFile.path }).then(() => { - expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); - }); - }); - - it('updates file raw data', () => { - return store.dispatch('getRawFileData', { path: tmpFile.path }).then(() => { - expect(tmpFile.raw).toBe('raw'); - }); - }); - - it('calls also getBaseRawFileData service method', () => { - jest.spyOn(service, 'getBaseRawFileData').mockReturnValue(Promise.resolve('baseraw')); - - store.state.currentProjectId = 'gitlab-org/gitlab-ce'; - store.state.currentMergeRequestId = '1'; - store.state.projects = { - 'gitlab-org/gitlab-ce': { - mergeRequests: { - 1: { - baseCommitSha: 'SHA', - }, - }, - }, - }; - - tmpFile.mrChange = { new_file: false }; - - return store.dispatch('getRawFileData', { path: tmpFile.path }).then(() => { - expect(service.getBaseRawFileData).toHaveBeenCalledWith( - tmpFile, - 'gitlab-org/gitlab-ce', - 'SHA', - ); - expect(tmpFile.baseRaw).toBe('baseraw'); - }); - }); - - describe('sets file loading to true', () => { - let loadingWhenGettingRawData; - let loadingWhenGettingBaseRawData; - - beforeEach(() => { - loadingWhenGettingRawData = undefined; - loadingWhenGettingBaseRawData = undefined; - - jest.spyOn(service, 'getRawFileData').mockImplementation((f) => { - loadingWhenGettingRawData = f.loading; - return Promise.resolve('raw'); - }); - jest.spyOn(service, 'getBaseRawFileData').mockImplementation((f) => { - loadingWhenGettingBaseRawData = f.loading; - return Promise.resolve('rawBase'); - }); - }); - - it('when getting raw file data', async () => { - expect(tmpFile.loading).toBe(false); - - await store.dispatch('getRawFileData', { path: tmpFile.path }); - - expect(loadingWhenGettingRawData).toBe(true); - expect(tmpFile.loading).toBe(false); - }); - - it('when getting base raw file data', async () => { - tmpFile.mrChange = { new_file: false }; - - expect(tmpFile.loading).toBe(false); - - await store.dispatch('getRawFileData', { path: tmpFile.path }); - - expect(loadingWhenGettingBaseRawData).toBe(true); - expect(tmpFile.loading).toBe(false); - }); - - it('when file was already loading', async () => { - tmpFile.loading = true; - - await store.dispatch('getRawFileData', { path: tmpFile.path }); - - expect(loadingWhenGettingRawData).toBe(true); - expect(tmpFile.loading).toBe(false); - }); - }); - }); - - describe('return JSON', () => { - beforeEach(() => { - mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_OK, JSON.stringify({ test: '123' })); - }); - - it('does not parse returned JSON', () => { - return store.dispatch('getRawFileData', { path: tmpFile.path }).then(() => { - expect(tmpFile.raw).toEqual('{"test":"123"}'); - }); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock.onGet(/(.*)/).networkError(); - }); - - it('dispatches error action', () => { - const dispatch = jest.fn(); - - return actions - .getRawFileData( - { state: store.state, commit() {}, dispatch, getters: store.getters }, - { path: tmpFile.path }, - ) - .catch(() => { - expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occurred while loading the file content.', - action: expect.any(Function), - actionText: 'Please try again', - actionPayload: { - path: tmpFile.path, - }, - }); - }); - }); - - it('toggles loading off after error', async () => { - await expect(store.dispatch('getRawFileData', { path: tmpFile.path })).rejects.toThrow(); - - expect(tmpFile.loading).toBe(false); - }); - }); - }); - - describe('changeFileContent', () => { - let tmpFile; - let onFilesChange; - - beforeEach(() => { - tmpFile = file('tmpFile'); - tmpFile.content = '\n'; - tmpFile.raw = '\n'; - store.state.entries[tmpFile.path] = tmpFile; - onFilesChange = jest.fn(); - eventHub.$on('ide.files.change', onFilesChange); - }); - - it('updates file content', () => { - const content = 'content\n'; - - return store.dispatch('changeFileContent', { path: tmpFile.path, content }).then(() => { - expect(tmpFile.content).toBe('content\n'); - }); - }); - - it('does nothing if path does not exist', () => { - const content = 'content\n'; - - return store - .dispatch('changeFileContent', { path: 'not/a/real_file.txt', content }) - .then(() => { - expect(tmpFile.content).toBe('\n'); - }); - }); - - it('adds file into stagedFiles array', () => { - return store - .dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content', - }) - .then(() => { - expect(store.state.stagedFiles.length).toBe(1); - }); - }); - - it('adds file not more than once into stagedFiles array', () => { - return store - .dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content', - }) - .then(() => - store.dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content 123', - }), - ) - .then(() => { - expect(store.state.stagedFiles.length).toBe(1); - }); - }); - - it('removes file from changedFiles array if not changed', () => { - return store - .dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content\n', - }) - .then(() => - store.dispatch('changeFileContent', { - path: tmpFile.path, - content: '\n', - }), - ) - .then(() => { - expect(store.state.changedFiles.length).toBe(0); - }); - }); - - it('triggers ide.files.change', async () => { - expect(onFilesChange).not.toHaveBeenCalled(); - - await store.dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content\n', - }); - - expect(onFilesChange).toHaveBeenCalledWith(createTriggerUpdatePayload(tmpFile.path)); - }); - }); - - describe('with changed file', () => { - let tmpFile; - - beforeEach(() => { - tmpFile = file('tempFile'); - tmpFile.content = 'testing'; - tmpFile.raw = ORIGINAL_CONTENT; - - store.state.changedFiles.push(tmpFile); - store.state.entries[tmpFile.path] = tmpFile; - }); - - describe('restoreOriginalFile', () => { - it('resets file content', () => - store.dispatch('restoreOriginalFile', tmpFile.path).then(() => { - expect(tmpFile.content).toBe(ORIGINAL_CONTENT); - })); - - it('closes temp file and deletes it', () => { - tmpFile.tempFile = true; - tmpFile.opened = true; - tmpFile.parentPath = 'parentFile'; - store.state.entries.parentFile = file('parentFile'); - - actions.restoreOriginalFile(store, tmpFile.path); - - expect(store.dispatch).toHaveBeenCalledWith('closeFile', tmpFile); - expect(store.dispatch).toHaveBeenCalledWith('deleteEntry', tmpFile.path); - }); - - describe('with renamed file', () => { - beforeEach(() => { - Object.assign(tmpFile, { - prevPath: 'parentPath/old_name', - prevName: 'old_name', - prevParentPath: 'parentPath', - }); - - store.state.entries.parentPath = file('parentPath'); - - actions.restoreOriginalFile(store, tmpFile.path); - }); - - it('renames the file to its original name and closes it if it was open', () => { - expect(store.dispatch).toHaveBeenCalledWith('closeFile', tmpFile); - expect(store.dispatch).toHaveBeenCalledWith('renameEntry', { - path: 'tempFile', - name: 'old_name', - parentPath: 'parentPath', - }); - }); - - it('resets file content', () => { - expect(tmpFile.content).toBe(ORIGINAL_CONTENT); - }); - }); - }); - - describe('discardFileChanges', () => { - beforeEach(() => { - jest.spyOn(eventHub, '$on').mockImplementation(() => {}); - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - }); - - describe('with regular file', () => { - beforeEach(() => { - actions.discardFileChanges(store, tmpFile.path); - }); - - it('restores original file', () => { - expect(store.dispatch).toHaveBeenCalledWith('restoreOriginalFile', tmpFile.path); - }); - - it('removes file from changedFiles array', () => { - expect(store.state.changedFiles.length).toBe(0); - }); - - it('does not push a new route', () => { - expect(router.push).not.toHaveBeenCalled(); - }); - - it('emits eventHub event to dispose cached model', () => { - actions.discardFileChanges(store, tmpFile.path); - - expect(eventHub.$emit).toHaveBeenCalledWith( - `editor.update.model.new.content.${tmpFile.key}`, - ORIGINAL_CONTENT, - ); - expect(eventHub.$emit).toHaveBeenCalledWith( - `editor.update.model.dispose.unstaged-${tmpFile.key}`, - ORIGINAL_CONTENT, - ); - }); - }); - - describe('with active file', () => { - beforeEach(() => { - tmpFile.active = true; - store.state.openFiles.push(tmpFile); - - actions.discardFileChanges(store, tmpFile.path); - }); - - it('pushes route for active file', () => { - expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/main/-/tempFile/'); - }); - }); - }); - }); - - describe('stageChange', () => { - it('calls STAGE_CHANGE with file path', () => { - const f = { ...file('path'), content: 'old' }; - - store.state.entries[f.path] = f; - - actions.stageChange(store, 'path'); - - expect(store.commit).toHaveBeenCalledWith( - types.STAGE_CHANGE, - expect.objectContaining({ path: 'path' }), - ); - expect(store.commit).toHaveBeenCalledWith(types.SET_LAST_COMMIT_MSG, ''); - }); - }); - - describe('unstageChange', () => { - it('calls UNSTAGE_CHANGE with file path', () => { - const f = { ...file('path'), content: 'old' }; - - store.state.entries[f.path] = f; - store.state.stagedFiles.push({ f, content: 'new' }); - - actions.unstageChange(store, 'path'); - - expect(store.commit).toHaveBeenCalledWith( - types.UNSTAGE_CHANGE, - expect.objectContaining({ path: 'path' }), - ); - }); - }); - - describe('openPendingTab', () => { - let f; - - beforeEach(() => { - f = { - ...file(), - projectId: '123', - }; - - store.state.entries[f.path] = f; - }); - - it('makes file pending in openFiles', () => { - return store.dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }).then(() => { - expect(store.state.openFiles[0].pending).toBe(true); - }); - }); - - it('returns true when opened', () => { - return store.dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }).then((added) => { - expect(added).toBe(true); - }); - }); - - it('returns false when already opened', () => { - store.state.openFiles.push({ - ...f, - active: true, - key: `pending-${f.key}`, - }); - - return store.dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }).then((added) => { - expect(added).toBe(false); - }); - }); - - it('pushes router URL when added', () => { - return store.dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }).then(() => { - expect(router.push).toHaveBeenCalledWith('/project/test/test/tree/main/'); - }); - }); - }); - - describe('removePendingTab', () => { - let f; - - beforeEach(() => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - - f = { - ...file('pendingFile'), - pending: true, - }; - }); - - it('removes pending file from open files', () => { - store.state.openFiles.push(f); - - return store.dispatch('removePendingTab', f).then(() => { - expect(store.state.openFiles.length).toBe(0); - }); - }); - - it('emits event to dispose model', () => { - return store.dispatch('removePendingTab', f).then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.${f.key}`); - }); - }); - }); - - describe('triggerFilesChange', () => { - const { payload: renamePayload } = createTriggerRenameAction('test', '123'); - - beforeEach(() => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - }); - - it.each` - args | payload - ${[]} | ${{}} - ${[renamePayload]} | ${renamePayload} - `('emits event that files have changed (args=$args)', ({ args, payload }) => { - return store.dispatch('triggerFilesChange', ...args).then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('ide.files.change', payload); - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js deleted file mode 100644 index a41ffdb0a31..00000000000 --- a/spec/frontend/ide/stores/actions/merge_request_spec.js +++ /dev/null @@ -1,532 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { range } from 'lodash'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import { TEST_HOST } from 'helpers/test_constants'; -import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/alert'; -import { leftSidebarViews, PERMISSION_READ_MR, MAX_MR_FILES_AUTO_OPEN } from '~/ide/constants'; -import service from '~/ide/services'; -import { createStore } from '~/ide/stores'; -import { - getMergeRequestData, - getMergeRequestChanges, - getMergeRequestVersions, - openMergeRequestChanges, - openMergeRequest, -} from '~/ide/stores/actions/merge_request'; -import * as types from '~/ide/stores/mutation_types'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; - -const TEST_PROJECT = 'abcproject'; -const TEST_PROJECT_ID = 17; - -const createMergeRequestChange = (path) => ({ - new_path: path, - path, -}); -const createMergeRequestChangesCount = (n) => - range(n).map((i) => createMergeRequestChange(`loremispum_${i}.md`)); - -const testGetUrlForPath = (path) => `${TEST_HOST}/test/${path}`; - -jest.mock('~/alert'); - -describe('IDE store merge request actions', () => { - let store; - let mock; - - beforeEach(() => { - stubPerformanceWebAPI(); - - store = createStore(); - - mock = new MockAdapter(axios); - - store.state.projects[TEST_PROJECT] = { - id: TEST_PROJECT_ID, - mergeRequests: {}, - userPermissions: { - [PERMISSION_READ_MR]: true, - }, - }; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('getMergeRequestsForBranch', () => { - describe('success', () => { - const mrData = { iid: 2, source_branch: 'bar' }; - const mockData = [mrData]; - - describe('base case', () => { - beforeEach(() => { - jest.spyOn(service, 'getProjectMergeRequests'); - mock - .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/) - .reply(HTTP_STATUS_OK, mockData); - }); - - it('calls getProjectMergeRequests service method', async () => { - await store.dispatch('getMergeRequestsForBranch', { - projectId: TEST_PROJECT, - branchId: 'bar', - }); - expect(service.getProjectMergeRequests).toHaveBeenCalledWith(TEST_PROJECT, { - source_branch: 'bar', - source_project_id: TEST_PROJECT_ID, - state: 'opened', - order_by: 'created_at', - per_page: 1, - }); - }); - - it('sets the "Merge Request" Object', async () => { - await store.dispatch('getMergeRequestsForBranch', { - projectId: TEST_PROJECT, - branchId: 'bar', - }); - expect(store.state.projects.abcproject.mergeRequests).toEqual({ - 2: expect.objectContaining(mrData), - }); - }); - - it('sets "Current Merge Request" object to the most recent MR', async () => { - await store.dispatch('getMergeRequestsForBranch', { - projectId: TEST_PROJECT, - branchId: 'bar', - }); - expect(store.state.currentMergeRequestId).toEqual('2'); - }); - - it('does nothing if user cannot read MRs', async () => { - store.state.projects[TEST_PROJECT].userPermissions[PERMISSION_READ_MR] = false; - - await store.dispatch('getMergeRequestsForBranch', { - projectId: TEST_PROJECT, - branchId: 'bar', - }); - expect(service.getProjectMergeRequests).not.toHaveBeenCalled(); - expect(store.state.currentMergeRequestId).toBe(''); - }); - }); - - describe('no merge requests for branch available case', () => { - beforeEach(() => { - jest.spyOn(service, 'getProjectMergeRequests'); - mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(HTTP_STATUS_OK, []); - }); - - it('does not fail if there are no merge requests for current branch', async () => { - await store.dispatch('getMergeRequestsForBranch', { - projectId: TEST_PROJECT, - branchId: 'foo', - }); - expect(store.state.projects[TEST_PROJECT].mergeRequests).toEqual({}); - expect(store.state.currentMergeRequestId).toEqual(''); - }); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).networkError(); - }); - - it('shows an alert, if error', () => { - return store - .dispatch('getMergeRequestsForBranch', { - projectId: TEST_PROJECT, - branchId: 'bar', - }) - .catch(() => { - expect(createAlert).toHaveBeenCalled(); - expect(createAlert.mock.calls[0][0].message).toBe( - 'Error fetching merge requests for bar', - ); - }); - }); - }); - }); - - describe('getMergeRequestData', () => { - describe('success', () => { - beforeEach(() => { - jest.spyOn(service, 'getProjectMergeRequestData'); - - mock - .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/) - .reply(HTTP_STATUS_OK, { title: 'mergerequest' }); - }); - - it('calls getProjectMergeRequestData service method', async () => { - await store.dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 }); - expect(service.getProjectMergeRequestData).toHaveBeenCalledWith(TEST_PROJECT, 1); - }); - - it('sets the Merge Request Object', async () => { - await store.dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 }); - expect(store.state.currentMergeRequestId).toBe(1); - expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].title).toBe('mergerequest'); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/).networkError(); - }); - - it('dispatches error action', () => { - const dispatch = jest.fn(); - - return getMergeRequestData( - { - commit() {}, - dispatch, - state: store.state, - }, - { projectId: TEST_PROJECT, mergeRequestId: 1 }, - ).catch(() => { - expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occurred while loading the merge request.', - action: expect.any(Function), - actionText: 'Please try again', - actionPayload: { - projectId: TEST_PROJECT, - mergeRequestId: 1, - force: false, - }, - }); - }); - }); - }); - }); - - describe('getMergeRequestChanges', () => { - beforeEach(() => { - store.state.projects[TEST_PROJECT].mergeRequests['1'] = { changes: [] }; - }); - - describe('success', () => { - beforeEach(() => { - jest.spyOn(service, 'getProjectMergeRequestChanges'); - - mock - .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/) - .reply(HTTP_STATUS_OK, { title: 'mergerequest' }); - }); - - it('calls getProjectMergeRequestChanges service method', async () => { - await store.dispatch('getMergeRequestChanges', { - projectId: TEST_PROJECT, - mergeRequestId: 1, - }); - expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith(TEST_PROJECT, 1); - }); - - it('sets the Merge Request Changes Object', async () => { - await store.dispatch('getMergeRequestChanges', { - projectId: TEST_PROJECT, - mergeRequestId: 1, - }); - expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].changes.title).toBe( - 'mergerequest', - ); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/).networkError(); - }); - - it('dispatches error action', async () => { - const dispatch = jest.fn(); - - await expect( - getMergeRequestChanges( - { - commit() {}, - dispatch, - state: store.state, - }, - { projectId: TEST_PROJECT, mergeRequestId: 1 }, - ), - ).rejects.toEqual(new Error('Merge request changes not loaded abcproject')); - - expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occurred while loading the merge request changes.', - action: expect.any(Function), - actionText: 'Please try again', - actionPayload: { - projectId: TEST_PROJECT, - mergeRequestId: 1, - force: false, - }, - }); - }); - }); - }); - - describe('getMergeRequestVersions', () => { - beforeEach(() => { - store.state.projects[TEST_PROJECT].mergeRequests['1'] = { versions: [] }; - }); - - describe('success', () => { - beforeEach(() => { - mock - .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/) - .reply(HTTP_STATUS_OK, [{ id: 789 }]); - jest.spyOn(service, 'getProjectMergeRequestVersions'); - }); - - it('calls getProjectMergeRequestVersions service method', async () => { - await store.dispatch('getMergeRequestVersions', { - projectId: TEST_PROJECT, - mergeRequestId: 1, - }); - expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith(TEST_PROJECT, 1); - }); - - it('sets the Merge Request Versions Object', async () => { - await store.dispatch('getMergeRequestVersions', { - projectId: TEST_PROJECT, - mergeRequestId: 1, - }); - expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].versions.length).toBe(1); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/).networkError(); - }); - - it('dispatches error action', () => { - const dispatch = jest.fn(); - - return getMergeRequestVersions( - { - commit() {}, - dispatch, - state: store.state, - }, - { projectId: TEST_PROJECT, mergeRequestId: 1 }, - ).catch(() => { - expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occurred while loading the merge request version data.', - action: expect.any(Function), - actionText: 'Please try again', - actionPayload: { - projectId: TEST_PROJECT, - mergeRequestId: 1, - force: false, - }, - }); - }); - }); - }); - }); - - describe('openMergeRequestChanges', () => { - it.each` - desc | changes | entries - ${'with empty changes'} | ${[]} | ${{}} - ${'with changes not matching entries'} | ${[{ new_path: '123.md' }]} | ${{ '456.md': {} }} - `('$desc, does nothing', ({ changes, entries }) => { - const state = { entries }; - - return testAction({ - action: openMergeRequestChanges, - state, - payload: changes, - expectedActions: [], - expectedMutations: [], - }); - }); - - it('updates views and opens mr changes', () => { - // This is the payload sent to the action - const changesPayload = createMergeRequestChangesCount(15); - - // Remove some items from the payload to use for entries - const changes = changesPayload.slice(1, 14); - - const entries = changes.reduce( - (acc, { path }) => Object.assign(acc, { [path]: path, type: 'blob' }), - {}, - ); - const pathsToOpen = changes.slice(0, MAX_MR_FILES_AUTO_OPEN).map((x) => x.new_path); - - return testAction({ - action: openMergeRequestChanges, - state: { entries, getUrlForPath: testGetUrlForPath }, - payload: changesPayload, - expectedActions: [ - { type: 'updateActivityBarView', payload: leftSidebarViews.review.name }, - // Only activates first file - { type: 'router/push', payload: testGetUrlForPath(pathsToOpen[0]) }, - { type: 'setFileActive', payload: pathsToOpen[0] }, - // Fetches data for other files - ...pathsToOpen.slice(1).map((path) => ({ - type: 'getFileData', - payload: { path, makeFileActive: false }, - })), - ...pathsToOpen.slice(1).map((path) => ({ - type: 'getRawFileData', - payload: { path }, - })), - ], - expectedMutations: [ - ...changes.map((change) => ({ - type: types.SET_FILE_MERGE_REQUEST_CHANGE, - payload: { - file: entries[change.new_path], - mrChange: change, - }, - })), - ...pathsToOpen.map((path) => ({ - type: types.TOGGLE_FILE_OPEN, - payload: path, - })), - ], - }); - }); - }); - - describe('openMergeRequest', () => { - const mr = { - projectId: TEST_PROJECT, - targetProjectId: 'defproject', - mergeRequestId: 2, - }; - let testMergeRequest; - let testMergeRequestChanges; - - const mockGetters = { findBranch: () => ({ commit: { id: 'abcd2322' } }) }; - - beforeEach(() => { - testMergeRequest = { - source_branch: 'abcbranch', - }; - testMergeRequestChanges = { - changes: [], - }; - store.state.entries = { - foo: { - type: 'blob', - }, - bar: { - type: 'blob', - }, - }; - - store.state.currentProjectId = 'test/test'; - store.state.currentBranchId = 'main'; - - store.state.projects['test/test'] = { - branches: { - main: { - commit: { - id: '7297abc', - }, - }, - abcbranch: { - commit: { - id: '29020fc', - }, - }, - }, - }; - - const originalDispatch = store.dispatch; - - jest.spyOn(store, 'dispatch').mockImplementation((type, payload) => { - switch (type) { - case 'getMergeRequestData': - return Promise.resolve(testMergeRequest); - case 'getMergeRequestChanges': - return Promise.resolve(testMergeRequestChanges); - case 'getFiles': - case 'getMergeRequestVersions': - case 'getBranchData': - return Promise.resolve(); - default: - return originalDispatch(type, payload); - } - }); - jest.spyOn(service, 'getFileData').mockImplementation(() => - Promise.resolve({ - headers: {}, - }), - ); - }); - - it('dispatches actions for merge request data', async () => { - await openMergeRequest( - { state: store.state, dispatch: store.dispatch, getters: mockGetters }, - mr, - ); - expect(store.dispatch.mock.calls).toEqual([ - ['getMergeRequestData', mr], - ['setCurrentBranchId', testMergeRequest.source_branch], - [ - 'getBranchData', - { - projectId: mr.projectId, - branchId: testMergeRequest.source_branch, - }, - ], - [ - 'getFiles', - { - projectId: mr.projectId, - branchId: testMergeRequest.source_branch, - ref: 'abcd2322', - }, - ], - ['getMergeRequestVersions', mr], - ['getMergeRequestChanges', mr], - ['openMergeRequestChanges', testMergeRequestChanges.changes], - ]); - }); - - it('updates activity bar view and gets file data, if changes are found', async () => { - store.state.entries.foo = { - type: 'blob', - path: 'foo', - }; - store.state.entries.bar = { - type: 'blob', - path: 'bar', - }; - - testMergeRequestChanges.changes = [ - { new_path: 'foo', path: 'foo' }, - { new_path: 'bar', path: 'bar' }, - ]; - - await openMergeRequest( - { state: store.state, dispatch: store.dispatch, getters: mockGetters }, - mr, - ); - expect(store.dispatch).toHaveBeenCalledWith( - 'openMergeRequestChanges', - testMergeRequestChanges.changes, - ); - }); - - it('shows an alert, if error', () => { - store.dispatch.mockRejectedValue(); - - return openMergeRequest(store, mr).catch(() => { - expect(createAlert).toHaveBeenCalledWith({ - message: expect.any(String), - }); - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js deleted file mode 100644 index b13228c20f5..00000000000 --- a/spec/frontend/ide/stores/actions/project_spec.js +++ /dev/null @@ -1,452 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; -import testAction from 'helpers/vuex_action_helper'; -import api from '~/api'; -import { createAlert } from '~/alert'; -import service from '~/ide/services'; -import { createStore } from '~/ide/stores'; -import { - setProject, - fetchProjectPermissions, - refreshLastCommitData, - showBranchNotFoundError, - createNewBranchFromDefault, - loadEmptyBranch, - openBranch, - loadFile, - loadBranch, -} from '~/ide/stores/actions'; -import { logError } from '~/lib/logger'; -import axios from '~/lib/utils/axios_utils'; - -jest.mock('~/alert'); -jest.mock('~/lib/logger'); - -const TEST_PROJECT_ID = 'abc/def'; - -describe('IDE store project actions', () => { - let mock; - let store; - - beforeEach(() => { - store = createStore(); - mock = new MockAdapter(axios); - - store.state.projects[TEST_PROJECT_ID] = { - branches: {}, - }; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('setProject', () => { - const project = { id: 'foo', path_with_namespace: TEST_PROJECT_ID }; - const baseMutations = [ - { - type: 'SET_PROJECT', - payload: { - projectPath: TEST_PROJECT_ID, - project, - }, - }, - { - type: 'SET_CURRENT_PROJECT', - payload: TEST_PROJECT_ID, - }, - ]; - - it.each` - desc | payload | expectedMutations - ${'does not commit any action if project is not passed'} | ${undefined} | ${[]} - ${'commits correct actions in the correct order by default'} | ${{ project }} | ${[...baseMutations]} - `('$desc', async ({ payload, expectedMutations } = {}) => { - await testAction({ - action: setProject, - payload, - state: store.state, - expectedMutations, - expectedActions: [], - }); - }); - }); - - describe('fetchProjectPermissions', () => { - const permissionsData = { - userPermissions: { - bogus: true, - }, - }; - const permissionsMutations = [ - { - type: 'UPDATE_PROJECT', - payload: { - projectPath: TEST_PROJECT_ID, - props: { - ...permissionsData, - }, - }, - }, - ]; - - let spy; - - beforeEach(() => { - spy = jest.spyOn(service, 'getProjectPermissionsData'); - }); - - afterEach(() => { - createAlert.mockRestore(); - }); - - it.each` - desc | projectPath | responseSuccess | expectedMutations - ${'does not fetch permissions if project does not exist'} | ${undefined} | ${true} | ${[]} - ${'fetches permission when project is specified'} | ${TEST_PROJECT_ID} | ${true} | ${[...permissionsMutations]} - ${'alerts an error if the request fails'} | ${TEST_PROJECT_ID} | ${false} | ${[]} - `('$desc', async ({ projectPath, expectedMutations, responseSuccess } = {}) => { - store.state.currentProjectId = projectPath; - if (responseSuccess) { - spy.mockResolvedValue(permissionsData); - } else { - spy.mockRejectedValue(); - } - - await testAction({ - action: fetchProjectPermissions, - state: store.state, - expectedMutations, - expectedActions: [], - }); - - if (!responseSuccess) { - expect(logError).toHaveBeenCalled(); - expect(createAlert).toHaveBeenCalled(); - } - }); - }); - - describe('refreshLastCommitData', () => { - beforeEach(() => { - store.state.currentProjectId = 'abc/def'; - store.state.currentBranchId = 'main'; - store.state.projects['abc/def'] = { - id: 4, - branches: { - main: { - commit: null, - }, - }, - }; - jest.spyOn(service, 'getBranchData').mockResolvedValue({ - data: { - commit: { id: '123' }, - }, - }); - }); - - it('calls the service', async () => { - await store.dispatch('refreshLastCommitData', { - projectId: store.state.currentProjectId, - branchId: store.state.currentBranchId, - }); - expect(service.getBranchData).toHaveBeenCalledWith('abc/def', 'main'); - }); - - it('commits getBranchData', () => { - return testAction( - refreshLastCommitData, - { - projectId: store.state.currentProjectId, - branchId: store.state.currentBranchId, - }, - store.state, - // mutations - [ - { - type: 'SET_BRANCH_COMMIT', - payload: { - projectId: TEST_PROJECT_ID, - branchId: 'main', - commit: { id: '123' }, - }, - }, - ], - // action - [], - ); - }); - }); - - describe('showBranchNotFoundError', () => { - it('dispatches setErrorMessage', () => { - return testAction( - showBranchNotFoundError, - 'main', - null, - [], - [ - { - type: 'setErrorMessage', - payload: { - text: "Branch main was not found in this project's repository.", - action: expect.any(Function), - actionText: 'Create branch', - actionPayload: 'main', - }, - }, - ], - ); - }); - }); - - describe('createNewBranchFromDefault', () => { - useMockLocationHelper(); - - beforeEach(() => { - jest.spyOn(api, 'createBranch').mockResolvedValue(); - }); - - it('calls API', async () => { - await createNewBranchFromDefault( - { - state: { - currentProjectId: 'project-path', - }, - getters: { - currentProject: { - default_branch: 'main', - }, - }, - dispatch() {}, - }, - 'new-branch-name', - ); - expect(api.createBranch).toHaveBeenCalledWith('project-path', { - ref: 'main', - branch: 'new-branch-name', - }); - }); - - it('clears error message', async () => { - const dispatchSpy = jest.fn().mockName('dispatch'); - - await createNewBranchFromDefault( - { - state: { - currentProjectId: 'project-path', - }, - getters: { - currentProject: { - default_branch: 'main', - }, - }, - dispatch: dispatchSpy, - }, - 'new-branch-name', - ); - expect(dispatchSpy).toHaveBeenCalledWith('setErrorMessage', null); - }); - - it('reloads window', async () => { - await createNewBranchFromDefault( - { - state: { - currentProjectId: 'project-path', - }, - getters: { - currentProject: { - default_branch: 'main', - }, - }, - dispatch() {}, - }, - 'new-branch-name', - ); - expect(window.location.reload).toHaveBeenCalled(); - }); - }); - - describe('loadEmptyBranch', () => { - it('creates a blank tree and sets loading state to false', () => { - return testAction( - loadEmptyBranch, - { projectId: TEST_PROJECT_ID, branchId: 'main' }, - store.state, - [ - { type: 'CREATE_TREE', payload: { treePath: `${TEST_PROJECT_ID}/main` } }, - { - type: 'TOGGLE_LOADING', - payload: { entry: store.state.trees[`${TEST_PROJECT_ID}/main`], forceValue: false }, - }, - ], - expect.any(Object), - ); - }); - - it('does nothing, if tree already exists', () => { - const trees = { [`${TEST_PROJECT_ID}/main`]: [] }; - - return testAction( - loadEmptyBranch, - { projectId: TEST_PROJECT_ID, branchId: 'main' }, - { trees }, - [], - [], - ); - }); - }); - - describe('loadFile', () => { - beforeEach(() => { - Object.assign(store.state, { - entries: { - foo: { pending: false }, - 'foo/bar-pending': { pending: true }, - 'foo/bar': { pending: false }, - }, - }); - jest.spyOn(store, 'dispatch').mockImplementation(); - }); - - it('does nothing, if basePath is not given', () => { - loadFile(store, { basePath: undefined }); - - expect(store.dispatch).not.toHaveBeenCalled(); - }); - - it('handles tree entry action, if basePath is given and the entry is not pending', () => { - loadFile(store, { basePath: 'foo/bar/' }); - - expect(store.dispatch).toHaveBeenCalledWith( - 'handleTreeEntryAction', - store.state.entries['foo/bar'], - ); - }); - - it('does not handle tree entry action, if entry is pending', () => { - loadFile(store, { basePath: 'foo/bar-pending/' }); - - expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', expect.anything()); - }); - - it('creates a new temp file supplied via URL if the file does not exist yet', () => { - loadFile(store, { basePath: 'not-existent.md' }); - - expect(store.dispatch.mock.calls).toHaveLength(1); - - expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', expect.anything()); - - expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { - name: 'not-existent.md', - type: 'blob', - }); - }); - }); - - describe('loadBranch', () => { - const projectId = TEST_PROJECT_ID; - const branchId = '123-lorem'; - const ref = 'abcd2322'; - - it('when empty repo, loads empty branch', () => { - const mockGetters = { emptyRepo: true }; - - return testAction( - loadBranch, - { projectId, branchId }, - { ...store.state, ...mockGetters }, - [], - [{ type: 'loadEmptyBranch', payload: { projectId, branchId } }], - ); - }); - - it('when branch already exists, does nothing', () => { - store.state.projects[projectId].branches[branchId] = {}; - - return testAction(loadBranch, { projectId, branchId }, store.state, [], []); - }); - - it('fetches branch data', async () => { - const mockGetters = { findBranch: () => ({ commit: { id: ref } }) }; - jest.spyOn(store, 'dispatch').mockResolvedValue(); - - await loadBranch( - { getters: mockGetters, state: store.state, dispatch: store.dispatch }, - { projectId, branchId }, - ); - expect(store.dispatch.mock.calls).toEqual([ - ['getBranchData', { projectId, branchId }], - ['getMergeRequestsForBranch', { projectId, branchId }], - ['getFiles', { projectId, branchId, ref }], - ]); - }); - - it('shows an error if branch can not be fetched', async () => { - jest.spyOn(store, 'dispatch').mockReturnValue(Promise.reject()); - - await expect(loadBranch(store, { projectId, branchId })).rejects.toBeUndefined(); - - expect(store.dispatch.mock.calls).toEqual([ - ['getBranchData', { projectId, branchId }], - ['showBranchNotFoundError', branchId], - ]); - }); - }); - - describe('openBranch', () => { - const projectId = TEST_PROJECT_ID; - const branchId = '123-lorem'; - - const branch = { - projectId, - branchId, - }; - - beforeEach(() => { - Object.assign(store.state, { - entries: { - foo: { pending: false }, - 'foo/bar-pending': { pending: true }, - 'foo/bar': { pending: false }, - }, - }); - }); - - describe('existing branch', () => { - beforeEach(() => { - jest.spyOn(store, 'dispatch').mockResolvedValue(); - }); - - it('dispatches branch actions', async () => { - await openBranch(store, branch); - expect(store.dispatch.mock.calls).toEqual([ - ['setCurrentBranchId', branchId], - ['loadBranch', { projectId, branchId }], - ['loadFile', { basePath: undefined }], - ]); - }); - }); - - describe('non-existent branch', () => { - beforeEach(() => { - jest.spyOn(store, 'dispatch').mockReturnValue(Promise.reject()); - }); - - it('dispatches correct branch actions', async () => { - const val = await openBranch(store, branch); - expect(store.dispatch.mock.calls).toEqual([ - ['setCurrentBranchId', branchId], - ['loadBranch', { projectId, branchId }], - ]); - - expect(val).toEqual( - new Error( - `An error occurred while getting files for - ${projectId}/${branchId}`, - ), - ); - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js deleted file mode 100644 index 47b6ebb3376..00000000000 --- a/spec/frontend/ide/stores/actions/tree_spec.js +++ /dev/null @@ -1,196 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import { TEST_HOST } from 'helpers/test_constants'; -import testAction from 'helpers/vuex_action_helper'; -import { createRouter } from '~/ide/ide_router'; -import service from '~/ide/services'; -import { createStore } from '~/ide/stores'; -import { showTreeEntry, getFiles, setDirectoryData } from '~/ide/stores/actions/tree'; -import * as types from '~/ide/stores/mutation_types'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { file, createEntriesFromPaths } from '../../helpers'; - -describe('Multi-file store tree actions', () => { - let projectTree; - let mock; - let store; - let router; - - const basicCallParameters = { - endpoint: 'rootEndpoint', - projectId: 'abcproject', - branch: 'main', - branchId: 'main', - ref: '12345678', - }; - - beforeEach(() => { - stubPerformanceWebAPI(); - - store = createStore(); - router = createRouter(store); - jest.spyOn(router, 'push').mockImplementation(); - - mock = new MockAdapter(axios); - - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'main'; - store.state.projects.abcproject = { - web_url: '', - path_with_namespace: 'foo/abcproject', - }; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('getFiles', () => { - describe('success', () => { - beforeEach(() => { - jest.spyOn(service, 'getFiles'); - - mock - .onGet(/(.*)/) - .replyOnce(HTTP_STATUS_OK, [ - 'file.txt', - 'folder/fileinfolder.js', - 'folder/subfolder/fileinsubfolder.js', - ]); - }); - - it('calls service getFiles', () => { - return store.dispatch('getFiles', basicCallParameters).then(() => { - expect(service.getFiles).toHaveBeenCalledWith('foo/abcproject', '12345678'); - }); - }); - - it('adds data into tree', async () => { - await store.dispatch('getFiles', basicCallParameters); - projectTree = store.state.trees['abcproject/main']; - - expect(projectTree.tree.length).toBe(2); - expect(projectTree.tree[0].type).toBe('tree'); - expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js'); - expect(projectTree.tree[1].type).toBe('blob'); - expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob'); - expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js'); - }); - }); - - describe('error', () => { - it('dispatches error action', async () => { - const dispatch = jest.fn(); - - store.state.projects = { - 'abc/def': { - web_url: `${TEST_HOST}/files`, - branches: { - 'main-testing': { - commit: { - id: '12345', - }, - }, - }, - }, - }; - const getters = { - findBranch: () => store.state.projects['abc/def'].branches['main-testing'], - }; - - mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - - await expect( - getFiles( - { - commit() {}, - dispatch, - state: store.state, - getters, - }, - { - projectId: 'abc/def', - branchId: 'main-testing', - }, - ), - ).rejects.toEqual(new Error('Request failed with status code 500')); - expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occurred while loading all the files.', - action: expect.any(Function), - actionText: 'Please try again', - actionPayload: { projectId: 'abc/def', branchId: 'main-testing' }, - }); - }); - }); - }); - - describe('toggleTreeOpen', () => { - let tree; - - beforeEach(() => { - tree = file('testing', '1', 'tree'); - store.state.entries[tree.path] = tree; - }); - - it('toggles the tree open', async () => { - await store.dispatch('toggleTreeOpen', tree.path); - expect(tree.opened).toBe(true); - }); - }); - - describe('showTreeEntry', () => { - beforeEach(() => { - const paths = [ - 'grandparent', - 'ancestor', - 'grandparent/parent', - 'grandparent/aunt', - 'grandparent/parent/child.txt', - 'grandparent/aunt/cousing.txt', - ]; - - Object.assign(store.state.entries, createEntriesFromPaths(paths)); - }); - - it('opens the parents', () => { - return testAction( - showTreeEntry, - 'grandparent/parent/child.txt', - store.state, - [{ type: types.SET_TREE_OPEN, payload: 'grandparent/parent' }], - [{ type: 'showTreeEntry', payload: 'grandparent/parent' }], - ); - }); - }); - - describe('setDirectoryData', () => { - it('sets tree correctly if there are no opened files yet', () => { - const treeFile = file({ name: 'README.md' }); - store.state.trees['abcproject/main'] = {}; - - return testAction( - setDirectoryData, - { projectId: 'abcproject', branchId: 'main', treeList: [treeFile] }, - store.state, - [ - { - type: types.SET_DIRECTORY_DATA, - payload: { - treePath: 'abcproject/main', - data: [treeFile], - }, - }, - { - type: types.TOGGLE_LOADING, - payload: { - entry: {}, - forceValue: false, - }, - }, - ], - [], - ); - }); - }); -}); diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js deleted file mode 100644 index f6925e78b6a..00000000000 --- a/spec/frontend/ide/stores/actions_spec.js +++ /dev/null @@ -1,942 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import testAction from 'helpers/vuex_action_helper'; -import eventHub from '~/ide/eventhub'; -import { createRouter } from '~/ide/ide_router'; -import { createStore } from '~/ide/stores'; -import { createAlert } from '~/alert'; -import { - init, - stageAllChanges, - unstageAllChanges, - toggleFileFinder, - setCurrentBranchId, - setEmptyStateSvgs, - updateActivityBarView, - updateTempFlagForEntry, - setErrorMessage, - deleteEntry, - renameEntry, - getBranchData, - createTempEntry, - discardAllChanges, -} from '~/ide/stores/actions'; -import * as types from '~/ide/stores/mutation_types'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_IM_A_TEAPOT, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status'; -import { visitUrl } from '~/lib/utils/url_utility'; -import { file, createTriggerRenameAction, createTriggerChangeAction } from '../helpers'; - -jest.mock('~/lib/utils/url_utility', () => ({ - visitUrl: jest.fn(), - joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths, -})); -jest.mock('~/alert'); - -describe('Multi-file store actions', () => { - let store; - let router; - - beforeEach(() => { - stubPerformanceWebAPI(); - - store = createStore(); - router = createRouter(store); - - jest.spyOn(store, 'commit'); - jest.spyOn(store, 'dispatch'); - jest.spyOn(router, 'push').mockImplementation(); - }); - - describe('redirectToUrl', () => { - it('calls visitUrl', async () => { - await store.dispatch('redirectToUrl', 'test'); - expect(visitUrl).toHaveBeenCalledWith('test'); - }); - }); - - describe('init', () => { - it('commits initial data and requests user callouts', () => { - return testAction( - init, - { canCommit: true }, - store.state, - [{ type: 'SET_INITIAL_DATA', payload: { canCommit: true } }], - [], - ); - }); - }); - - describe('discardAllChanges', () => { - const paths = ['to_discard', 'another_one_to_discard']; - - beforeEach(() => { - paths.forEach((path) => { - const f = file(path); - f.changed = true; - - store.state.openFiles.push(f); - store.state.changedFiles.push(f); - store.state.entries[f.path] = f; - }); - }); - - it('discards all changes in file', () => { - const expectedCalls = paths.map((path) => ['restoreOriginalFile', path]); - - discardAllChanges(store); - - expect(store.dispatch.mock.calls).toEqual(expect.arrayContaining(expectedCalls)); - }); - - it('removes all files from changedFiles state', async () => { - await store.dispatch('discardAllChanges'); - expect(store.state.changedFiles.length).toBe(0); - expect(store.state.openFiles.length).toBe(2); - }); - }); - - describe('createTempEntry', () => { - beforeEach(() => { - document.body.innerHTML += '
    '; - - store.state.currentProjectId = 'abcproject'; - store.state.currentBranchId = 'mybranch'; - - store.state.trees['abcproject/mybranch'] = { - tree: [], - }; - store.state.projects.abcproject = { - web_url: '', - }; - }); - - afterEach(() => { - document.querySelector('.flash-container').remove(); - }); - - describe('tree', () => { - it('creates temp tree', async () => { - await store.dispatch('createTempEntry', { - name: 'test', - type: 'tree', - }); - const entry = store.state.entries.test; - - expect(entry).not.toBeNull(); - expect(entry.type).toBe('tree'); - }); - - it('creates new folder inside another tree', async () => { - const tree = { - type: 'tree', - name: 'testing', - path: 'testing', - tree: [], - }; - - store.state.entries[tree.path] = tree; - - await store.dispatch('createTempEntry', { - name: 'testing/test', - type: 'tree', - }); - expect(tree.tree[0].tempFile).toBe(true); - expect(tree.tree[0].name).toBe('test'); - expect(tree.tree[0].type).toBe('tree'); - }); - - it('does not create new tree if already exists', async () => { - const tree = { - type: 'tree', - path: 'testing', - tempFile: false, - tree: [], - }; - - store.state.entries[tree.path] = tree; - - await store.dispatch('createTempEntry', { - name: 'testing', - type: 'tree', - }); - expect(store.state.entries[tree.path].tempFile).toEqual(false); - expect(createAlert).toHaveBeenCalled(); - }); - }); - - describe('blob', () => { - it('creates temp file', async () => { - const name = 'test'; - - await store.dispatch('createTempEntry', { - name, - type: 'blob', - mimeType: 'test/mime', - }); - const f = store.state.entries[name]; - - expect(f.tempFile).toBe(true); - expect(f.mimeType).toBe('test/mime'); - expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1); - }); - - it('adds tmp file to open files', async () => { - const name = 'test'; - - await store.dispatch('createTempEntry', { - name, - type: 'blob', - }); - const f = store.state.entries[name]; - - expect(store.state.openFiles.length).toBe(1); - expect(store.state.openFiles[0].name).toBe(f.name); - }); - - it('adds tmp file to staged files', async () => { - const name = 'test'; - - await store.dispatch('createTempEntry', { - name, - type: 'blob', - }); - expect(store.state.stagedFiles).toEqual([expect.objectContaining({ name })]); - }); - - it('sets tmp file as active', () => { - createTempEntry(store, { name: 'test', type: 'blob' }); - - expect(store.dispatch).toHaveBeenCalledWith('setFileActive', 'test'); - }); - - it('creates alert if file already exists', async () => { - const f = file('test', '1', 'blob'); - store.state.trees['abcproject/mybranch'].tree = [f]; - store.state.entries[f.path] = f; - - await store.dispatch('createTempEntry', { - name: 'test', - type: 'blob', - }); - expect(createAlert).toHaveBeenCalledWith( - expect.objectContaining({ - message: `The name "${f.name}" is already taken in this directory.`, - }), - ); - }); - }); - }); - - describe('scrollToTab', () => { - it('focuses the current active element', () => { - document.body.innerHTML += - '
    '; - const el = document.querySelector('.repo-tab'); - jest.spyOn(el, 'focus').mockImplementation(); - - return store.dispatch('scrollToTab').then(() => { - expect(el.focus).toHaveBeenCalled(); - - document.getElementById('tabs').remove(); - }); - }); - }); - - describe('stage/unstageAllChanges', () => { - let file1; - let file2; - - beforeEach(() => { - file1 = { ...file('test'), content: 'changed test', raw: 'test' }; - file2 = { ...file('test2'), content: 'changed test2', raw: 'test2' }; - - store.state.openFiles = [file1]; - store.state.changedFiles = [file1]; - store.state.stagedFiles = [{ ...file2, content: 'staged test' }]; - - store.state.entries = { - [file1.path]: { ...file1 }, - [file2.path]: { ...file2 }, - }; - }); - - describe('stageAllChanges', () => { - it('adds all files from changedFiles to stagedFiles', () => { - stageAllChanges(store); - - expect(store.commit.mock.calls).toEqual( - expect.arrayContaining([ - [types.SET_LAST_COMMIT_MSG, ''], - [types.STAGE_CHANGE, expect.objectContaining({ path: file1.path })], - ]), - ); - }); - - it('opens pending tab if a change exists in that file', () => { - stageAllChanges(store); - - expect(store.dispatch.mock.calls).toEqual([ - [ - 'openPendingTab', - { file: { ...file1, staged: true, changed: true }, keyPrefix: 'staged' }, - ], - ]); - }); - - it('does not open pending tab if no change exists in that file', () => { - store.state.entries[file1.path].content = 'test'; - store.state.stagedFiles = [file1]; - store.state.changedFiles = [store.state.entries[file1.path]]; - - stageAllChanges(store); - - expect(store.dispatch).not.toHaveBeenCalled(); - }); - }); - - describe('unstageAllChanges', () => { - it('removes all files from stagedFiles after unstaging', () => { - unstageAllChanges(store); - - expect(store.commit.mock.calls).toEqual( - expect.arrayContaining([ - [types.UNSTAGE_CHANGE, expect.objectContaining({ path: file2.path })], - ]), - ); - }); - - it('opens pending tab if a change exists in that file', () => { - unstageAllChanges(store); - - expect(store.dispatch.mock.calls).toEqual([ - ['openPendingTab', { file: file1, keyPrefix: 'unstaged' }], - ]); - }); - - it('does not open pending tab if no change exists in that file', () => { - store.state.entries[file1.path].content = 'test'; - store.state.stagedFiles = [file1]; - store.state.changedFiles = [store.state.entries[file1.path]]; - - unstageAllChanges(store); - - expect(store.dispatch).not.toHaveBeenCalled(); - }); - }); - }); - - describe('updateViewer', () => { - it('updates viewer state', async () => { - await store.dispatch('updateViewer', 'diff'); - expect(store.state.viewer).toBe('diff'); - }); - }); - - describe('updateActivityBarView', () => { - it('commits UPDATE_ACTIVITY_BAR_VIEW', () => { - return testAction( - updateActivityBarView, - 'test', - {}, - [{ type: 'UPDATE_ACTIVITY_BAR_VIEW', payload: 'test' }], - [], - ); - }); - }); - - describe('setEmptyStateSvgs', () => { - it('commits setEmptyStateSvgs', () => { - return testAction( - setEmptyStateSvgs, - 'svg', - {}, - [{ type: 'SET_EMPTY_STATE_SVGS', payload: 'svg' }], - [], - ); - }); - }); - - describe('updateTempFlagForEntry', () => { - it('commits UPDATE_TEMP_FLAG', () => { - const f = { - ...file(), - path: 'test', - tempFile: true, - }; - store.state.entries[f.path] = f; - - return testAction( - updateTempFlagForEntry, - { file: f, tempFile: false }, - store.state, - [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }], - [], - ); - }); - - it('commits UPDATE_TEMP_FLAG and dispatches for parent', () => { - const parent = { - ...file(), - path: 'testing', - }; - const f = { - ...file(), - path: 'test', - parentPath: 'testing', - }; - store.state.entries[parent.path] = parent; - store.state.entries[f.path] = f; - - return testAction( - updateTempFlagForEntry, - { file: f, tempFile: false }, - store.state, - [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }], - [{ type: 'updateTempFlagForEntry', payload: { file: parent, tempFile: false } }], - ); - }); - - it('does not dispatch for parent, if parent does not exist', () => { - const f = { - ...file(), - path: 'test', - parentPath: 'testing', - }; - store.state.entries[f.path] = f; - - return testAction( - updateTempFlagForEntry, - { file: f, tempFile: false }, - store.state, - [{ type: 'UPDATE_TEMP_FLAG', payload: { path: f.path, tempFile: false } }], - [], - ); - }); - }); - - describe('setCurrentBranchId', () => { - it('commits setCurrentBranchId', () => { - return testAction( - setCurrentBranchId, - 'branchId', - {}, - [{ type: 'SET_CURRENT_BRANCH', payload: 'branchId' }], - [], - ); - }); - }); - - describe('toggleFileFinder', () => { - it('commits TOGGLE_FILE_FINDER', () => { - return testAction( - toggleFileFinder, - true, - null, - [{ type: 'TOGGLE_FILE_FINDER', payload: true }], - [], - ); - }); - }); - - describe('setErrorMessage', () => { - it('commis error message', () => { - return testAction( - setErrorMessage, - 'error', - null, - [{ type: types.SET_ERROR_MESSAGE, payload: 'error' }], - [], - ); - }); - }); - - describe('deleteEntry', () => { - it('commits entry deletion', () => { - store.state.entries.path = 'testing'; - - return testAction( - deleteEntry, - 'path', - store.state, - [{ type: types.DELETE_ENTRY, payload: 'path' }], - [{ type: 'stageChange', payload: 'path' }, createTriggerChangeAction()], - ); - }); - - it('does not delete a folder after it is emptied', () => { - const testFolder = { - type: 'tree', - tree: [], - }; - const testEntry = { - path: 'testFolder/entry-to-delete', - parentPath: 'testFolder', - opened: false, - tree: [], - }; - testFolder.tree.push(testEntry); - store.state.entries = { - testFolder, - 'testFolder/entry-to-delete': testEntry, - }; - - return testAction( - deleteEntry, - 'testFolder/entry-to-delete', - store.state, - [{ type: types.DELETE_ENTRY, payload: 'testFolder/entry-to-delete' }], - [ - { type: 'stageChange', payload: 'testFolder/entry-to-delete' }, - createTriggerChangeAction(), - ], - ); - }); - - describe('when renamed', () => { - let testEntry; - - beforeEach(() => { - testEntry = { - path: 'test', - name: 'test', - prevPath: 'test_old', - prevName: 'test_old', - prevParentPath: '', - }; - - store.state.entries = { test: testEntry }; - }); - - describe('and previous does not exist', () => { - it('reverts the rename before deleting', () => { - return testAction( - deleteEntry, - testEntry.path, - store.state, - [], - [ - { - type: 'renameEntry', - payload: { - path: testEntry.path, - name: testEntry.prevName, - parentPath: testEntry.prevParentPath, - }, - }, - { - type: 'deleteEntry', - payload: testEntry.prevPath, - }, - ], - ); - }); - }); - - describe('and previous exists', () => { - beforeEach(() => { - const oldEntry = { - path: testEntry.prevPath, - name: testEntry.prevName, - }; - - store.state.entries[oldEntry.path] = oldEntry; - }); - - it('does not revert rename before deleting', () => { - return testAction( - deleteEntry, - testEntry.path, - store.state, - [{ type: types.DELETE_ENTRY, payload: testEntry.path }], - [{ type: 'stageChange', payload: testEntry.path }, createTriggerChangeAction()], - ); - }); - - it('when previous is deleted, it reverts rename before deleting', () => { - store.state.entries[testEntry.prevPath].deleted = true; - - return testAction( - deleteEntry, - testEntry.path, - store.state, - [], - [ - { - type: 'renameEntry', - payload: { - path: testEntry.path, - name: testEntry.prevName, - parentPath: testEntry.prevParentPath, - }, - }, - { - type: 'deleteEntry', - payload: testEntry.prevPath, - }, - ], - ); - }); - }); - }); - }); - - describe('renameEntry', () => { - describe('purging of file model cache', () => { - beforeEach(() => { - jest.spyOn(eventHub, '$emit').mockImplementation(); - }); - - it('does not purge model cache for temporary entries that got renamed', async () => { - Object.assign(store.state.entries, { - test: { - ...file('test'), - key: 'foo-key', - type: 'blob', - tempFile: true, - }, - }); - - await store.dispatch('renameEntry', { - path: 'test', - name: 'new', - }); - expect(eventHub.$emit.mock.calls).not.toContain('editor.update.model.dispose.foo-bar'); - }); - - it('purges model cache for renamed entry', async () => { - Object.assign(store.state.entries, { - test: { - ...file('test'), - key: 'foo-key', - type: 'blob', - tempFile: false, - }, - }); - - await store.dispatch('renameEntry', { - path: 'test', - name: 'new', - }); - expect(eventHub.$emit).toHaveBeenCalled(); - expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.foo-key`); - }); - }); - - describe('single entry', () => { - let origEntry; - let renamedEntry; - - beforeEach(() => { - // Need to insert both because `testAction` doesn't actually call the mutation - origEntry = file('orig', 'orig', 'blob'); - renamedEntry = { - ...file('renamed', 'renamed', 'blob'), - prevKey: origEntry.key, - prevName: origEntry.name, - prevPath: origEntry.path, - }; - - Object.assign(store.state.entries, { - orig: origEntry, - renamed: renamedEntry, - }); - }); - - it('by default renames an entry and stages it', () => { - const dispatch = jest.fn(); - const commit = jest.fn(); - - renameEntry( - { dispatch, commit, state: store.state, getters: store.getters }, - { path: 'orig', name: 'renamed' }, - ); - - expect(commit.mock.calls).toEqual([ - [types.RENAME_ENTRY, { path: 'orig', name: 'renamed', parentPath: undefined }], - [types.STAGE_CHANGE, expect.objectContaining({ path: 'renamed' })], - ]); - }); - - it('if not changed, completely unstages and discards entry if renamed to original', () => { - return testAction( - renameEntry, - { path: 'renamed', name: 'orig' }, - store.state, - [ - { - type: types.RENAME_ENTRY, - payload: { - path: 'renamed', - name: 'orig', - parentPath: undefined, - }, - }, - { - type: types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, - payload: origEntry, - }, - ], - [createTriggerRenameAction('renamed', 'orig')], - ); - }); - - it('if already in changed, does not add to change', () => { - store.state.changedFiles.push(renamedEntry); - - return testAction( - renameEntry, - { path: 'orig', name: 'renamed' }, - store.state, - [expect.objectContaining({ type: types.RENAME_ENTRY })], - [createTriggerRenameAction('orig', 'renamed')], - ); - }); - - it('routes to the renamed file if the original file has been opened', async () => { - store.state.currentProjectId = 'test/test'; - store.state.currentBranchId = 'main'; - - Object.assign(store.state.entries.orig, { - opened: true, - }); - - await store.dispatch('renameEntry', { - path: 'orig', - name: 'renamed', - }); - expect(router.push.mock.calls).toHaveLength(1); - expect(router.push).toHaveBeenCalledWith(`/project/test/test/tree/main/-/renamed/`); - }); - }); - - describe('folder', () => { - let folder; - let file1; - let file2; - - beforeEach(() => { - folder = file('folder', 'folder', 'tree'); - file1 = file('file-1', 'file-1', 'blob', folder); - file2 = file('file-2', 'file-2', 'blob', folder); - - folder.tree = [file1, file2]; - - Object.assign(store.state.entries, { - [folder.path]: folder, - [file1.path]: file1, - [file2.path]: file2, - }); - }); - - it('updates entries in a folder correctly, when folder is renamed', async () => { - await store.dispatch('renameEntry', { - path: 'folder', - name: 'new-folder', - }); - const keys = Object.keys(store.state.entries); - - expect(keys.length).toBe(3); - expect(keys.indexOf('new-folder')).toBe(0); - expect(keys.indexOf('new-folder/file-1')).toBe(1); - expect(keys.indexOf('new-folder/file-2')).toBe(2); - }); - - it('discards renaming of an entry if the root folder is renamed back to a previous name', async () => { - const rootFolder = file('old-folder', 'old-folder', 'tree'); - const testEntry = file('test', 'test', 'blob', rootFolder); - - Object.assign(store.state, { - entries: { - 'old-folder': { - ...rootFolder, - tree: [testEntry], - }, - 'old-folder/test': testEntry, - }, - }); - - await store.dispatch('renameEntry', { - path: 'old-folder', - name: 'new-folder', - }); - const { entries } = store.state; - - expect(Object.keys(entries).length).toBe(2); - expect(entries['old-folder']).toBeUndefined(); - expect(entries['old-folder/test']).toBeUndefined(); - - expect(entries['new-folder']).toBeDefined(); - expect(entries['new-folder/test']).toEqual( - expect.objectContaining({ - path: 'new-folder/test', - name: 'test', - prevPath: 'old-folder/test', - prevName: 'test', - }), - ); - - await store.dispatch('renameEntry', { - path: 'new-folder', - name: 'old-folder', - }); - const { entries: newEntries } = store.state; - - expect(Object.keys(newEntries).length).toBe(2); - expect(newEntries['new-folder']).toBeUndefined(); - expect(newEntries['new-folder/test']).toBeUndefined(); - - expect(newEntries['old-folder']).toBeDefined(); - expect(newEntries['old-folder/test']).toEqual( - expect.objectContaining({ - path: 'old-folder/test', - name: 'test', - prevPath: undefined, - prevName: undefined, - }), - ); - }); - - describe('with file in directory', () => { - const parentPath = 'original-dir'; - const newParentPath = 'new-dir'; - const fileName = 'test.md'; - const filePath = `${parentPath}/${fileName}`; - - let rootDir; - - beforeEach(() => { - const parentEntry = file(parentPath, parentPath, 'tree'); - const fileEntry = file(filePath, filePath, 'blob', parentEntry); - rootDir = { - tree: [], - }; - - Object.assign(store.state, { - entries: { - [parentPath]: { - ...parentEntry, - tree: [fileEntry], - }, - [filePath]: fileEntry, - }, - trees: { - '/': rootDir, - }, - }); - }); - - it('creates new directory', async () => { - expect(store.state.entries[newParentPath]).toBeUndefined(); - - await store.dispatch('renameEntry', { - path: filePath, - name: fileName, - parentPath: newParentPath, - }); - expect(store.state.entries[newParentPath]).toEqual( - expect.objectContaining({ - path: newParentPath, - type: 'tree', - tree: expect.arrayContaining([store.state.entries[`${newParentPath}/${fileName}`]]), - }), - ); - }); - - describe('when new directory exists', () => { - let newDir; - - beforeEach(() => { - newDir = file(newParentPath, newParentPath, 'tree'); - - store.state.entries[newDir.path] = newDir; - rootDir.tree.push(newDir); - }); - - it('inserts in new directory', async () => { - expect(newDir.tree).toEqual([]); - - await store.dispatch('renameEntry', { - path: filePath, - name: fileName, - parentPath: newParentPath, - }); - expect(newDir.tree).toEqual([store.state.entries[`${newParentPath}/${fileName}`]]); - }); - - it('when new directory is deleted, it undeletes it', async () => { - await store.dispatch('deleteEntry', newParentPath); - - expect(store.state.entries[newParentPath].deleted).toBe(true); - expect(rootDir.tree.some((x) => x.path === newParentPath)).toBe(false); - - await store.dispatch('renameEntry', { - path: filePath, - name: fileName, - parentPath: newParentPath, - }); - expect(store.state.entries[newParentPath].deleted).toBe(false); - expect(rootDir.tree.some((x) => x.path === newParentPath)).toBe(true); - }); - }); - }); - }); - }); - - describe('getBranchData', () => { - let mock; - - beforeEach(() => { - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('error', () => { - let dispatch; - let callParams; - - beforeEach(() => { - callParams = [ - { - commit() {}, - state: store.state, - }, - { - projectId: 'abc/def', - branchId: 'main-testing', - }, - ]; - dispatch = jest.fn(); - document.body.innerHTML += '
    '; - }); - - afterEach(() => { - document.querySelector('.flash-container').remove(); - }); - - it('passes the error further unchanged without dispatching any action when response is 404', async () => { - mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_NOT_FOUND); - - await expect(getBranchData(...callParams)).rejects.toEqual( - new Error('Request failed with status code 404'), - ); - expect(dispatch.mock.calls).toHaveLength(0); - expect(document.querySelector('.flash-alert')).toBeNull(); - }); - - it('does not pass the error further and creates an alert if error is not 404', async () => { - mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_IM_A_TEAPOT); - - await expect(getBranchData(...callParams)).rejects.toEqual( - new Error('Branch not loaded - abc/def/main-testing'), - ); - - expect(dispatch.mock.calls).toHaveLength(0); - expect(createAlert).toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/extend_spec.js b/spec/frontend/ide/stores/extend_spec.js deleted file mode 100644 index 88909999c82..00000000000 --- a/spec/frontend/ide/stores/extend_spec.js +++ /dev/null @@ -1,71 +0,0 @@ -import extendStore from '~/ide/stores/extend'; -import terminalPlugin from '~/ide/stores/plugins/terminal'; -import terminalSyncPlugin from '~/ide/stores/plugins/terminal_sync'; - -jest.mock('~/ide/stores/plugins/terminal', () => jest.fn()); -jest.mock('~/ide/stores/plugins/terminal_sync', () => jest.fn()); - -describe('ide/stores/extend', () => { - let store; - let el; - - beforeEach(() => { - store = {}; - el = {}; - - [terminalPlugin, terminalSyncPlugin].forEach((x) => { - const plugin = jest.fn(); - - x.mockImplementation(() => plugin); - }); - }); - - afterEach(() => { - terminalPlugin.mockClear(); - terminalSyncPlugin.mockClear(); - }); - - const withGonFeatures = (features) => { - global.gon.features = features; - }; - - describe('terminalPlugin', () => { - beforeEach(() => { - extendStore(store, el); - }); - - it('is created', () => { - expect(terminalPlugin).toHaveBeenCalledWith(el); - }); - - it('is called with store', () => { - expect(terminalPlugin()).toHaveBeenCalledWith(store); - }); - }); - - describe('terminalSyncPlugin', () => { - describe('when buildServiceProxy feature is enabled', () => { - beforeEach(() => { - withGonFeatures({ buildServiceProxy: true }); - - extendStore(store, el); - }); - - it('is created', () => { - expect(terminalSyncPlugin).toHaveBeenCalledWith(el); - }); - - it('is called with store', () => { - expect(terminalSyncPlugin()).toHaveBeenCalledWith(store); - }); - }); - - describe('when buildServiceProxy feature is disabled', () => { - it('is not created', () => { - extendStore(store, el); - - expect(terminalSyncPlugin).not.toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js deleted file mode 100644 index d5fce5a0b8c..00000000000 --- a/spec/frontend/ide/stores/getters_spec.js +++ /dev/null @@ -1,644 +0,0 @@ -import { TEST_HOST } from 'helpers/test_constants'; -import { - DEFAULT_PERMISSIONS, - PERMISSION_PUSH_CODE, - PUSH_RULE_REJECT_UNSIGNED_COMMITS, -} from '~/ide/constants'; -import { - MSG_CANNOT_PUSH_CODE, - MSG_CANNOT_PUSH_CODE_GO_TO_FORK, - MSG_CANNOT_PUSH_CODE_SHOULD_FORK, - MSG_CANNOT_PUSH_UNSIGNED, - MSG_CANNOT_PUSH_UNSIGNED_SHORT, - MSG_FORK, - MSG_GO_TO_FORK, -} from '~/ide/messages'; -import { createStore } from '~/ide/stores'; -import * as getters from '~/ide/stores/getters'; -import { file } from '../helpers'; - -const TEST_PROJECT_ID = 'test_project'; -const TEST_IDE_PATH = '/test/ide/path'; -const TEST_FORK_PATH = '/test/fork/path'; - -describe('IDE store getters', () => { - let localState; - let localStore; - - beforeEach(() => { - // Feature flag is defaulted to on in prod - window.gon = { features: { rejectUnsignedCommitsByGitlab: true } }; - - localStore = createStore(); - localState = localStore.state; - }); - - describe('activeFile', () => { - it('returns the current active file', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('active')); - localState.openFiles[1].active = true; - - expect(getters.activeFile(localState).name).toBe('active'); - }); - - it('returns undefined if no active files are found', () => { - localState.openFiles.push(file()); - localState.openFiles.push(file('active')); - - expect(getters.activeFile(localState)).toBeNull(); - }); - }); - - describe('modifiedFiles', () => { - it('returns a list of modified files', () => { - localState.openFiles.push(file()); - localState.changedFiles.push(file('changed')); - localState.changedFiles[0].changed = true; - - const modifiedFiles = getters.modifiedFiles(localState); - - expect(modifiedFiles.length).toBe(1); - expect(modifiedFiles[0].name).toBe('changed'); - }); - }); - - describe('currentMergeRequest', () => { - it('returns Current Merge Request', () => { - localState.currentProjectId = 'abcproject'; - localState.currentMergeRequestId = 1; - localState.projects.abcproject = { - mergeRequests: { - 1: { - mergeId: 1, - }, - }, - }; - - expect(getters.currentMergeRequest(localState).mergeId).toBe(1); - }); - - it('returns null if no active Merge Request was found', () => { - localState.currentProjectId = 'otherproject'; - - expect(getters.currentMergeRequest(localState)).toBeNull(); - }); - }); - - describe('allBlobs', () => { - beforeEach(() => { - Object.assign(localState.entries, { - index: { - type: 'blob', - name: 'index', - lastOpenedAt: 0, - }, - app: { - type: 'blob', - name: 'blob', - lastOpenedAt: 0, - }, - folder: { - type: 'folder', - name: 'folder', - lastOpenedAt: 0, - }, - }); - }); - - it('returns only blobs', () => { - expect(getters.allBlobs(localState).length).toBe(2); - }); - - it('returns list sorted by lastOpenedAt', () => { - localState.entries.app.lastOpenedAt = new Date().getTime(); - - expect(getters.allBlobs(localState)[0].name).toBe('blob'); - }); - }); - - describe('getChangesInFolder', () => { - it('returns length of changed files for a path', () => { - localState.changedFiles.push( - { - path: 'test/index', - name: 'index', - }, - { - path: 'app/123', - name: '123', - }, - ); - - expect(getters.getChangesInFolder(localState)('test')).toBe(1); - }); - - it('returns length of changed & staged files for a path', () => { - localState.changedFiles.push( - { - path: 'test/index', - name: 'index', - }, - { - path: 'testing/123', - name: '123', - }, - ); - - localState.stagedFiles.push( - { - path: 'test/123', - name: '123', - }, - { - path: 'test/index', - name: 'index', - }, - { - path: 'testing/12345', - name: '12345', - }, - ); - - expect(getters.getChangesInFolder(localState)('test')).toBe(2); - }); - - it('returns length of changed & tempFiles files for a path', () => { - localState.changedFiles.push( - { - path: 'test/index', - name: 'index', - }, - { - path: 'test/newfile', - name: 'newfile', - tempFile: true, - }, - ); - - expect(getters.getChangesInFolder(localState)('test')).toBe(2); - }); - }); - - describe('lastCommit', () => { - it('returns the last commit of the current branch on the current project', () => { - const commitTitle = 'Example commit title'; - const localGetters = { - currentProject: { - name: 'test-project', - }, - currentBranch: { - commit: { - title: commitTitle, - }, - }, - }; - localState.currentBranchId = 'example-branch'; - - expect(getters.lastCommit(localState, localGetters).title).toBe(commitTitle); - }); - }); - - describe('currentBranch', () => { - it('returns current projects branch', () => { - localState.currentProjectId = 'abcproject'; - localState.currentBranchId = 'main'; - localState.projects.abcproject = { - name: 'abcproject', - branches: { - main: { - name: 'main', - }, - }, - }; - const localGetters = { - findBranch: jest.fn(), - }; - getters.currentBranch(localState, localGetters); - - expect(localGetters.findBranch).toHaveBeenCalledWith('abcproject', 'main'); - }); - }); - - describe('findProject', () => { - it('returns the project matching the id', () => { - localState.currentProjectId = 'abcproject'; - localState.projects.abcproject = { - name: 'abcproject', - }; - - expect(getters.findProject(localState)('abcproject').name).toBe('abcproject'); - }); - }); - - describe('findBranch', () => { - let result; - - it('returns the selected branch from a project', () => { - localState.currentProjectId = 'abcproject'; - localState.currentBranchId = 'main'; - localState.projects.abcproject = { - name: 'abcproject', - branches: { - main: { - name: 'main', - }, - }, - }; - const localGetters = { - findProject: () => localState.projects.abcproject, - }; - - result = getters.findBranch(localState, localGetters)('abcproject', 'main'); - - expect(result.name).toBe('main'); - }); - }); - - describe('isOnDefaultBranch', () => { - it('returns false when no project exists', () => { - const localGetters = { - currentProject: undefined, - }; - - expect(getters.isOnDefaultBranch({}, localGetters)).toBe(undefined); - }); - - it("returns true when project's default branch matches current branch", () => { - const localGetters = { - currentProject: { - default_branch: 'main', - }, - branchName: 'main', - }; - - expect(getters.isOnDefaultBranch({}, localGetters)).toBe(true); - }); - - it("returns false when project's default branch doesn't match current branch", () => { - const localGetters = { - currentProject: { - default_branch: 'main', - }, - branchName: 'feature', - }; - - expect(getters.isOnDefaultBranch({}, localGetters)).toBe(false); - }); - }); - - describe('canPushToBranch', () => { - it.each` - currentBranch | canPushCode | expectedValue - ${undefined} | ${undefined} | ${false} - ${{ can_push: true }} | ${false} | ${true} - ${{ can_push: true }} | ${true} | ${true} - ${{ can_push: false }} | ${false} | ${false} - ${{ can_push: false }} | ${true} | ${false} - ${undefined} | ${true} | ${true} - ${undefined} | ${false} | ${false} - `( - 'with currentBranch ($currentBranch) and canPushCode ($canPushCode), it is $expectedValue', - ({ currentBranch, canPushCode, expectedValue }) => { - expect(getters.canPushToBranch({}, { currentBranch, canPushCode })).toBe(expectedValue); - }, - ); - }); - - describe('isFileDeletedAndReadded', () => { - const f = { ...file('sample'), content: 'sample', raw: 'sample' }; - - it.each([ - { - entry: { ...f, tempFile: true }, - staged: { ...f, deleted: true }, - output: true, - }, - { - entry: { ...f, content: 'changed' }, - staged: { ...f, content: 'changed' }, - output: false, - }, - { - entry: { ...f, content: 'changed' }, - output: false, - }, - ])( - 'checks staged and unstaged files to see if a file was deleted and readded (case %#)', - ({ entry, staged, output }) => { - Object.assign(localState, { - entries: { - [entry.path]: entry, - }, - stagedFiles: [], - }); - - if (staged) localState.stagedFiles.push(staged); - - expect(localStore.getters.isFileDeletedAndReadded(entry.path)).toBe(output); - }, - ); - }); - - describe('getDiffInfo', () => { - const f = { ...file('sample'), content: 'sample', raw: 'sample' }; - it.each([ - { - entry: { ...f, tempFile: true }, - staged: { ...f, deleted: true }, - output: { deleted: false, changed: false, tempFile: false }, - }, - { - entry: { ...f, tempFile: true, content: 'changed', raw: '' }, - staged: { ...f, deleted: true }, - output: { deleted: false, changed: true, tempFile: false }, - }, - { - entry: { ...f, content: 'changed' }, - output: { changed: true }, - }, - { - entry: { ...f, content: 'sample' }, - staged: { ...f, content: 'changed' }, - output: { changed: false }, - }, - { - entry: { ...f, deleted: true }, - output: { deleted: true, changed: false }, - }, - { - entry: { ...f, prevPath: 'old_path' }, - output: { renamed: true, changed: false }, - }, - { - entry: { ...f, prevPath: 'old_path', content: 'changed' }, - output: { renamed: true, changed: true }, - }, - ])( - 'compares changes in a file entry and returns a resulting diff info (case %#)', - ({ entry, staged, output }) => { - Object.assign(localState, { - entries: { - [entry.path]: entry, - }, - stagedFiles: [], - }); - - if (staged) localState.stagedFiles.push(staged); - - expect(localStore.getters.getDiffInfo(entry.path)).toEqual(expect.objectContaining(output)); - }, - ); - }); - - describe.each` - getterName | projectField | defaultValue - ${'findProjectPermissions'} | ${'userPermissions'} | ${DEFAULT_PERMISSIONS} - ${'findPushRules'} | ${'pushRules'} | ${{}} - `('$getterName', ({ getterName, projectField, defaultValue }) => { - const callGetter = (...args) => localStore.getters[getterName](...args); - - it('returns default if project not found', () => { - expect(callGetter(TEST_PROJECT_ID)).toEqual(defaultValue); - }); - - it('finds field in given project', () => { - const obj = { test: 'foo' }; - - localState.projects[TEST_PROJECT_ID] = { [projectField]: obj }; - - expect(callGetter(TEST_PROJECT_ID)).toStrictEqual(obj); - }); - }); - - describe.each` - getterName | permissionKey - ${'canReadMergeRequests'} | ${'readMergeRequest'} - ${'canCreateMergeRequests'} | ${'createMergeRequestIn'} - `('$getterName', ({ getterName, permissionKey }) => { - it.each([true, false])('finds permission for current project (%s)', (val) => { - localState.projects[TEST_PROJECT_ID] = { - userPermissions: { - [permissionKey]: val, - }, - }; - localState.currentProjectId = TEST_PROJECT_ID; - - expect(localStore.getters[getterName]).toBe(val); - }); - }); - - describe('canPushCodeStatus', () => { - it.each([ - [ - 'when can push code, and can push unsigned commits', - { - input: { pushCode: true, rejectUnsignedCommits: false }, - output: { isAllowed: true, message: '', messageShort: '' }, - }, - ], - [ - 'when cannot push code, and can push unsigned commits', - { - input: { pushCode: false, rejectUnsignedCommits: false }, - output: { - isAllowed: false, - message: MSG_CANNOT_PUSH_CODE, - messageShort: MSG_CANNOT_PUSH_CODE, - }, - }, - ], - [ - 'when cannot push code, and has ide_path in forkInfo', - { - input: { - pushCode: false, - rejectUnsignedCommits: false, - forkInfo: { ide_path: TEST_IDE_PATH }, - }, - output: { - isAllowed: false, - message: MSG_CANNOT_PUSH_CODE_GO_TO_FORK, - messageShort: MSG_CANNOT_PUSH_CODE, - action: { href: TEST_IDE_PATH, text: MSG_GO_TO_FORK }, - }, - }, - ], - [ - 'when cannot push code, and has fork_path in forkInfo', - { - input: { - pushCode: false, - rejectUnsignedCommits: false, - forkInfo: { fork_path: TEST_FORK_PATH }, - }, - output: { - isAllowed: false, - message: MSG_CANNOT_PUSH_CODE_SHOULD_FORK, - messageShort: MSG_CANNOT_PUSH_CODE, - action: { href: TEST_FORK_PATH, text: MSG_FORK, isForm: true }, - }, - }, - ], - [ - 'when can push code, but cannot push unsigned commits', - { - input: { pushCode: true, rejectUnsignedCommits: true }, - output: { - isAllowed: false, - message: MSG_CANNOT_PUSH_UNSIGNED, - messageShort: MSG_CANNOT_PUSH_UNSIGNED_SHORT, - }, - }, - ], - [ - 'when can push code, but cannot push unsigned commits, with reject_unsigned_commits_by_gitlab feature off', - { - input: { - pushCode: true, - rejectUnsignedCommits: true, - features: { rejectUnsignedCommitsByGitlab: false }, - }, - output: { - isAllowed: true, - message: '', - messageShort: '', - }, - }, - ], - ])('%s', (testName, { input, output }) => { - const { forkInfo, rejectUnsignedCommits, pushCode, features = {} } = input; - - Object.assign(window.gon.features, features); - localState.links = { forkInfo }; - localState.projects[TEST_PROJECT_ID] = { - pushRules: { - [PUSH_RULE_REJECT_UNSIGNED_COMMITS]: rejectUnsignedCommits, - }, - userPermissions: { - [PERMISSION_PUSH_CODE]: pushCode, - }, - }; - localState.currentProjectId = TEST_PROJECT_ID; - - expect(localStore.getters.canPushCodeStatus).toEqual(output); - }); - }); - - describe('canPushCode', () => { - it.each([true, false])('with canPushCodeStatus.isAllowed = $s', (isAllowed) => { - const canPushCodeStatus = { isAllowed }; - - expect(getters.canPushCode({}, { canPushCodeStatus })).toBe(isAllowed); - }); - }); - - describe('entryExists', () => { - beforeEach(() => { - localState.entries = { - foo: file('foo', 'foo', 'tree'), - 'foo/bar.png': file(), - }; - }); - - it.each` - path | deleted | value - ${'foo/bar.png'} | ${false} | ${true} - ${'foo/bar.png'} | ${true} | ${false} - ${'foo'} | ${false} | ${true} - `( - 'returns $value for an existing entry path: $path (deleted: $deleted)', - ({ path, deleted, value }) => { - localState.entries[path].deleted = deleted; - - expect(localStore.getters.entryExists(path)).toBe(value); - }, - ); - - it('returns false for a non existing entry path', () => { - expect(localStore.getters.entryExists('bar.baz')).toBe(false); - }); - }); - - describe('getAvailableFileName', () => { - it.each` - path | newPath - ${'foo'} | ${'foo-1'} - ${'foo__93.png'} | ${'foo__94.png'} - ${'foo/bar.png'} | ${'foo/bar-1.png'} - ${'foo/bar--34.png'} | ${'foo/bar--35.png'} - ${'foo/bar 2.png'} | ${'foo/bar 3.png'} - ${'foo/bar-621.png'} | ${'foo/bar-622.png'} - ${'jquery.min.js'} | ${'jquery-1.min.js'} - ${'my_spec_22.js.snap'} | ${'my_spec_23.js.snap'} - ${'subtitles5.mp4.srt'} | ${'subtitles-6.mp4.srt'} - ${'sample-file.mp3'} | ${'sample-file-1.mp3'} - ${'Screenshot 2020-05-26 at 10.53.08 PM.png'} | ${'Screenshot 2020-05-26 at 11.53.08 PM.png'} - `('suffixes the path with a number if the path already exists', ({ path, newPath }) => { - localState.entries[path] = file(); - - expect(localStore.getters.getAvailableFileName(path)).toBe(newPath); - }); - - it('loops through all incremented entries and keeps trying until a file path that does not exist is found', () => { - localState.entries = { - 'bar/baz_1.png': file(), - 'bar/baz_2.png': file(), - 'bar/baz_3.png': file(), - 'bar/baz_4.png': file(), - 'bar/baz_5.png': file(), - 'bar/baz_72.png': file(), - }; - - expect(localStore.getters.getAvailableFileName('bar/baz_1.png')).toBe('bar/baz_6.png'); - }); - - it('returns the entry path as is if the path does not exist', () => { - expect(localStore.getters.getAvailableFileName('foo-bar1.jpg')).toBe('foo-bar1.jpg'); - }); - }); - - describe('getUrlForPath', () => { - it('returns a route url for the given path', () => { - localState.currentProjectId = 'test/test'; - localState.currentBranchId = 'main'; - - expect(localStore.getters.getUrlForPath('path/to/foo/bar-1.jpg')).toBe( - `/project/test/test/tree/main/-/path/to/foo/bar-1.jpg/`, - ); - }); - }); - - describe('getJsonSchemaForPath', () => { - beforeEach(() => { - localState.currentProjectId = 'path/to/some/project'; - localState.currentBranchId = 'main'; - }); - - it('returns a json schema uri and match config for a json/yaml file that can be loaded by monaco', () => { - expect(localStore.getters.getJsonSchemaForPath('.gitlab-ci.yml')).toEqual({ - fileMatch: ['*.gitlab-ci.yml'], - uri: `${TEST_HOST}/path/to/some/project/-/schema/main/.gitlab-ci.yml`, - }); - }); - - it('returns a path containing sha if branch details are present in state', () => { - localState.projects['path/to/some/project'] = { - name: 'project', - branches: { - main: { - name: 'main', - commit: { - id: 'abcdef123456', - }, - }, - }, - }; - - expect(localStore.getters.getJsonSchemaForPath('.gitlab-ci.yml')).toEqual({ - fileMatch: ['*.gitlab-ci.yml'], - uri: `${TEST_HOST}/path/to/some/project/-/schema/abcdef123456/.gitlab-ci.yml`, - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/integration_spec.js b/spec/frontend/ide/stores/integration_spec.js deleted file mode 100644 index 388bd3b99d2..00000000000 --- a/spec/frontend/ide/stores/integration_spec.js +++ /dev/null @@ -1,93 +0,0 @@ -import { decorateFiles } from '~/ide/lib/files'; -import { createStore } from '~/ide/stores'; - -const TEST_BRANCH = 'test_branch'; -const TEST_NAMESPACE = 'test_namespace'; -const TEST_PROJECT_ID = `${TEST_NAMESPACE}/test_project`; -const TEST_PATH_DIR = 'src'; -const TEST_PATH = `${TEST_PATH_DIR}/foo.js`; -const TEST_CONTENT = `Lorem ipsum dolar sit -Lorem ipsum dolar -Lorem ipsum -Lorem -`; - -jest.mock('~/ide/ide_router'); - -describe('IDE store integration', () => { - let store; - - beforeEach(() => { - store = createStore(); - store.replaceState({ - ...store.state, - projects: { - [TEST_PROJECT_ID]: { - web_url: 'test_web_url', - branches: [], - }, - }, - currentProjectId: TEST_PROJECT_ID, - currentBranchId: TEST_BRANCH, - }); - }); - - describe('with project and files', () => { - beforeEach(() => { - const { entries, treeList } = decorateFiles({ - data: [`${TEST_PATH_DIR}/`, TEST_PATH, 'README.md'], - }); - - Object.assign(entries[TEST_PATH], { - raw: TEST_CONTENT, - }); - - store.replaceState({ - ...store.state, - trees: { - [`${TEST_PROJECT_ID}/${TEST_BRANCH}`]: { - tree: treeList, - }, - }, - entries, - }); - }); - - describe('when a file is deleted and readded', () => { - beforeEach(() => { - store.dispatch('deleteEntry', TEST_PATH); - store.dispatch('createTempEntry', { name: TEST_PATH, type: 'blob' }); - }); - - it('is added to staged as modified', () => { - expect(store.state.stagedFiles).toEqual([ - expect.objectContaining({ - path: TEST_PATH, - deleted: false, - staged: true, - changed: true, - tempFile: false, - }), - ]); - }); - - it('cleans up after commit', () => { - const expected = expect.objectContaining({ - path: TEST_PATH, - staged: false, - changed: false, - tempFile: false, - deleted: false, - }); - store.dispatch('stageChange', TEST_PATH); - - store.dispatch('commit/updateFilesAfterCommit', { data: {} }); - - expect(store.state.entries[TEST_PATH]).toEqual(expected); - expect(store.state.entries[TEST_PATH_DIR].tree.find((x) => x.path === TEST_PATH)).toEqual( - expected, - ); - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/branches/actions_spec.js b/spec/frontend/ide/stores/modules/branches/actions_spec.js deleted file mode 100644 index c1c47ef7e9a..00000000000 --- a/spec/frontend/ide/stores/modules/branches/actions_spec.js +++ /dev/null @@ -1,162 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; -import { - requestBranches, - receiveBranchesError, - receiveBranchesSuccess, - fetchBranches, - resetBranches, -} from '~/ide/stores/modules/branches/actions'; -import * as types from '~/ide/stores/modules/branches/mutation_types'; -import state from '~/ide/stores/modules/branches/state'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { branches, projectData } from '../../../mock_data'; - -describe('IDE branches actions', () => { - const TEST_SEARCH = 'foosearch'; - let mockedContext; - let mockedState; - let mock; - - beforeEach(() => { - mockedContext = { - dispatch() {}, - rootState: { currentProjectId: projectData.name_with_namespace }, - rootGetters: { currentProject: projectData }, - state: state(), - }; - - // testAction looks for rootGetters in state, - // so they need to be concatenated here. - mockedState = { - ...mockedContext.state, - ...mockedContext.rootGetters, - ...mockedContext.rootState, - }; - - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('requestBranches', () => { - it('should commit request', () => { - return testAction( - requestBranches, - null, - mockedContext.state, - [{ type: types.REQUEST_BRANCHES }], - [], - ); - }); - }); - - describe('receiveBranchesError', () => { - it('should commit error', () => { - return testAction( - receiveBranchesError, - { search: TEST_SEARCH }, - mockedContext.state, - [{ type: types.RECEIVE_BRANCHES_ERROR }], - [ - { - type: 'setErrorMessage', - payload: { - text: 'Error loading branches.', - action: expect.any(Function), - actionText: 'Please try again', - actionPayload: { search: TEST_SEARCH }, - }, - }, - ], - ); - }); - }); - - describe('receiveBranchesSuccess', () => { - it('should commit received data', () => { - return testAction( - receiveBranchesSuccess, - branches, - mockedContext.state, - [{ type: types.RECEIVE_BRANCHES_SUCCESS, payload: branches }], - [], - ); - }); - }); - - describe('fetchBranches', () => { - beforeEach(() => { - gon.api_version = 'v4'; - }); - - describe('success', () => { - beforeEach(() => { - mock - .onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/) - .replyOnce(HTTP_STATUS_OK, branches); - }); - - it('calls API with params', () => { - const apiSpy = jest.spyOn(axios, 'get'); - - fetchBranches(mockedContext, { search: TEST_SEARCH }); - - expect(apiSpy).toHaveBeenCalledWith(expect.anything(), { - params: expect.objectContaining({ search: TEST_SEARCH, sort: 'updated_desc' }), - }); - }); - - it('dispatches success with received data', () => { - return testAction( - fetchBranches, - { search: TEST_SEARCH }, - mockedState, - [], - [ - { type: 'requestBranches' }, - { type: 'resetBranches' }, - { type: 'receiveBranchesSuccess', payload: branches }, - ], - ); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock - .onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/) - .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - }); - - it('dispatches error', () => { - return testAction( - fetchBranches, - { search: TEST_SEARCH }, - mockedState, - [], - [ - { type: 'requestBranches' }, - { type: 'resetBranches' }, - { type: 'receiveBranchesError', payload: { search: TEST_SEARCH } }, - ], - ); - }); - }); - - describe('resetBranches', () => { - it('commits reset', () => { - return testAction( - resetBranches, - null, - mockedContext.state, - [{ type: types.RESET_BRANCHES }], - [], - ); - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/branches/mutations_spec.js b/spec/frontend/ide/stores/modules/branches/mutations_spec.js deleted file mode 100644 index fd6006749d2..00000000000 --- a/spec/frontend/ide/stores/modules/branches/mutations_spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import * as types from '~/ide/stores/modules/branches/mutation_types'; -import mutations from '~/ide/stores/modules/branches/mutations'; -import state from '~/ide/stores/modules/branches/state'; -import { branches } from '../../../mock_data'; - -describe('IDE branches mutations', () => { - let mockedState; - - beforeEach(() => { - mockedState = state(); - }); - - describe('REQUEST_BRANCHES', () => { - it('sets loading to true', () => { - mutations[types.REQUEST_BRANCHES](mockedState); - - expect(mockedState.isLoading).toBe(true); - }); - }); - - describe('RECEIVE_BRANCHES_ERROR', () => { - it('sets loading to false', () => { - mutations[types.RECEIVE_BRANCHES_ERROR](mockedState); - - expect(mockedState.isLoading).toBe(false); - }); - }); - - describe('RECEIVE_BRANCHES_SUCCESS', () => { - it('sets branches', () => { - const expectedBranches = branches.map((branch) => ({ - name: branch.name, - committedDate: branch.commit.committed_date, - })); - - mutations[types.RECEIVE_BRANCHES_SUCCESS](mockedState, branches); - - expect(mockedState.branches).toEqual(expectedBranches); - }); - }); - - describe('RESET_BRANCHES', () => { - it('clears branches array', () => { - mockedState.branches = ['test']; - - mutations[types.RESET_BRANCHES](mockedState); - - expect(mockedState.branches).toEqual([]); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js deleted file mode 100644 index 55814c3b986..00000000000 --- a/spec/frontend/ide/stores/modules/commit/actions_spec.js +++ /dev/null @@ -1,498 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import testAction from 'helpers/vuex_action_helper'; -import { file } from 'jest/ide/helpers'; -import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants'; -import eventHub from '~/ide/eventhub'; -import { createRouter } from '~/ide/ide_router'; -import { createUnexpectedCommitError } from '~/ide/lib/errors'; -import service from '~/ide/services'; -import { createStore } from '~/ide/stores'; -import * as actions from '~/ide/stores/modules/commit/actions'; -import { - COMMIT_TO_CURRENT_BRANCH, - COMMIT_TO_NEW_BRANCH, -} from '~/ide/stores/modules/commit/constants'; -import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types'; -import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { visitUrl } from '~/lib/utils/url_utility'; - -jest.mock('~/lib/utils/url_utility', () => ({ - ...jest.requireActual('~/lib/utils/url_utility'), - visitUrl: jest.fn(), -})); - -const TEST_COMMIT_SHA = '123456789'; -const COMMIT_RESPONSE = { - id: '123456', - short_id: '123', - message: 'test message', - committed_date: 'date', - parent_ids: [], - stats: { - additions: '1', - deletions: '2', - }, -}; - -describe('IDE commit module actions', () => { - let mock; - let store; - let router; - - beforeEach(() => { - store = createStore(); - router = createRouter(store); - gon.api_version = 'v1'; - mock = new MockAdapter(axios); - jest.spyOn(router, 'push').mockImplementation(); - - mock - .onGet('/api/v1/projects/abcproject/repository/branches/main') - .reply(HTTP_STATUS_OK, { commit: COMMIT_RESPONSE }); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('updateCommitMessage', () => { - it('updates store with new commit message', async () => { - await store.dispatch('commit/updateCommitMessage', 'testing'); - expect(store.state.commit.commitMessage).toBe('testing'); - }); - }); - - describe('discardDraft', () => { - it('resets commit message to blank', async () => { - store.state.commit.commitMessage = 'testing'; - - await store.dispatch('commit/discardDraft'); - expect(store.state.commit.commitMessage).not.toBe('testing'); - }); - }); - - describe('updateCommitAction', () => { - it('updates store with new commit action', async () => { - await store.dispatch('commit/updateCommitAction', '1'); - expect(store.state.commit.commitAction).toBe('1'); - }); - }); - - describe('updateBranchName', () => { - beforeEach(() => { - window.gon.current_username = 'johndoe'; - - store.state.currentBranchId = 'main'; - }); - - it('updates store with new branch name', async () => { - await store.dispatch('commit/updateBranchName', 'branch-name'); - - expect(store.state.commit.newBranchName).toBe('branch-name'); - }); - }); - - describe('addSuffixToBranchName', () => { - it('adds suffix to branchName', async () => { - jest.spyOn(Math, 'random').mockReturnValue(0.391352525); - - store.state.commit.newBranchName = 'branch-name'; - - await store.dispatch('commit/addSuffixToBranchName'); - - expect(store.state.commit.newBranchName).toBe('branch-name-39135'); - }); - }); - - describe('setLastCommitMessage', () => { - beforeEach(() => { - Object.assign(store.state, { - currentProjectId: 'abcproject', - projects: { - abcproject: { - web_url: 'http://testing', - }, - }, - }); - }); - - it('updates commit message with short_id', async () => { - await store.dispatch('commit/setLastCommitMessage', { short_id: '123' }); - expect(store.state.lastCommitMsg).toContain( - 'Your changes have been committed. Commit 123', - ); - }); - - it('updates commit message with stats', async () => { - await store.dispatch('commit/setLastCommitMessage', { - short_id: '123', - stats: { - additions: '1', - deletions: '2', - }, - }); - expect(store.state.lastCommitMsg).toBe( - 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.', - ); - }); - }); - - describe('updateFilesAfterCommit', () => { - const data = { - id: '123', - message: 'testing commit message', - committed_date: '123', - committer_name: 'root', - }; - const branch = 'main'; - let f; - - beforeEach(() => { - jest.spyOn(eventHub, '$emit').mockImplementation(); - - f = file('changedFile'); - Object.assign(f, { - active: true, - changed: true, - content: 'file content', - }); - - Object.assign(store.state, { - currentProjectId: 'abcproject', - currentBranchId: 'main', - projects: { - abcproject: { - web_url: 'web_url', - branches: { - main: { - workingReference: '', - commit: { - short_id: TEST_COMMIT_SHA, - }, - }, - }, - }, - }, - stagedFiles: [ - f, - { - ...file('changedFile2'), - changed: true, - }, - ], - }); - - store.state.openFiles = store.state.stagedFiles; - store.state.stagedFiles.forEach((stagedFile) => { - store.state.entries[stagedFile.path] = stagedFile; - }); - }); - - it('updates stores working reference', async () => { - await store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }); - expect(store.state.projects.abcproject.branches.main.workingReference).toBe(data.id); - }); - - it('resets all files changed status', async () => { - await store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }); - store.state.openFiles.forEach((entry) => { - expect(entry.changed).toBe(false); - }); - }); - - it('sets files commit data', async () => { - await store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }); - expect(f.lastCommitSha).toBe(data.id); - }); - - it('updates raw content for changed file', async () => { - await store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }); - expect(f.raw).toBe(f.content); - }); - - it('emits changed event for file', async () => { - await store.dispatch('commit/updateFilesAfterCommit', { - data, - branch, - }); - expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.content.${f.key}`, { - content: f.content, - changed: false, - }); - }); - }); - - describe('commitChanges', () => { - beforeEach(() => { - document.body.innerHTML += '
    '; - - const f = { - ...file('changed'), - type: 'blob', - active: true, - lastCommitSha: TEST_COMMIT_SHA, - content: '\n', - raw: '\n', - }; - - Object.assign(store.state, { - stagedFiles: [f], - changedFiles: [f], - openFiles: [f], - currentProjectId: 'abcproject', - currentBranchId: 'main', - projects: { - abcproject: { - default_branch: 'main', - web_url: 'webUrl', - branches: { - main: { - name: 'main', - workingReference: '1', - commit: { - id: TEST_COMMIT_SHA, - }, - can_push: true, - }, - }, - userPermissions: { - [PERMISSION_CREATE_MR]: true, - }, - }, - }, - }); - - store.state.commit.commitAction = '2'; - store.state.commit.commitMessage = 'testing 123'; - - store.state.openFiles.forEach((localF) => { - store.state.entries[localF.path] = localF; - }); - }); - - afterEach(() => { - document.querySelector('.flash-container').remove(); - }); - - describe('success', () => { - beforeEach(() => { - jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE }); - }); - - it('calls service', async () => { - await store.dispatch('commit/commitChanges'); - expect(service.commit).toHaveBeenCalledWith('abcproject', { - branch: expect.anything(), - commit_message: 'testing 123', - actions: [ - { - action: commitActionTypes.update, - file_path: expect.anything(), - content: '\n', - encoding: expect.anything(), - last_commit_id: undefined, - previous_path: undefined, - }, - ], - start_sha: TEST_COMMIT_SHA, - }); - }); - - it('sends lastCommit ID when not creating new branch', async () => { - store.state.commit.commitAction = '1'; - - await store.dispatch('commit/commitChanges'); - expect(service.commit).toHaveBeenCalledWith('abcproject', { - branch: expect.anything(), - commit_message: 'testing 123', - actions: [ - { - action: commitActionTypes.update, - file_path: expect.anything(), - content: '\n', - encoding: expect.anything(), - last_commit_id: TEST_COMMIT_SHA, - previous_path: undefined, - }, - ], - start_sha: undefined, - }); - }); - - it('sets last Commit Msg', async () => { - await store.dispatch('commit/commitChanges'); - expect(store.state.lastCommitMsg).toBe( - 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.', - ); - }); - - it('adds commit data to files', async () => { - await store.dispatch('commit/commitChanges'); - expect(store.state.entries[store.state.openFiles[0].path].lastCommitSha).toBe( - COMMIT_RESPONSE.id, - ); - }); - - it('resets stores commit actions', async () => { - store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH; - - await store.dispatch('commit/commitChanges'); - expect(store.state.commit.commitAction).not.toBe(COMMIT_TO_NEW_BRANCH); - }); - - it('removes all staged files', async () => { - await store.dispatch('commit/commitChanges'); - expect(store.state.stagedFiles.length).toBe(0); - }); - - describe('merge request', () => { - it.each` - branchName | targetBranchName | branchNameInURL | targetBranchInURL - ${'foo'} | ${'main'} | ${'foo'} | ${'main'} - ${'foo#bar'} | ${'main'} | ${'foo%23bar'} | ${'main'} - ${'foo#bar'} | ${'not#so#main'} | ${'foo%23bar'} | ${'not%23so%23main'} - `( - 'redirects to the correct new MR page when new branch is "$branchName" and target branch is "$targetBranchName"', - async ({ branchName, targetBranchName, branchNameInURL, targetBranchInURL }) => { - Object.assign(store.state.projects.abcproject, { - branches: { - [targetBranchName]: { - name: targetBranchName, - workingReference: '1', - commit: { - id: TEST_COMMIT_SHA, - }, - can_push: true, - }, - }, - }); - store.state.currentBranchId = targetBranchName; - store.state.commit.newBranchName = branchName; - - store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH; - store.state.commit.shouldCreateMR = true; - - await store.dispatch('commit/commitChanges'); - expect(visitUrl).toHaveBeenCalledWith( - `webUrl/-/merge_requests/new?merge_request[source_branch]=${branchNameInURL}&merge_request[target_branch]=${targetBranchInURL}&nav_source=webide`, - ); - }, - ); - - it('does not redirect to new merge request page when shouldCreateMR is not checked', async () => { - jest.spyOn(eventHub, '$on').mockImplementation(); - - store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH; - store.state.commit.shouldCreateMR = false; - - await store.dispatch('commit/commitChanges'); - expect(visitUrl).not.toHaveBeenCalled(); - }); - - it('does not redirect to merge request page if shouldCreateMR is checked, but branch is the default branch', async () => { - jest.spyOn(eventHub, '$on').mockImplementation(); - - store.state.commit.commitAction = COMMIT_TO_CURRENT_BRANCH; - store.state.commit.shouldCreateMR = true; - - await store.dispatch('commit/commitChanges'); - expect(visitUrl).not.toHaveBeenCalled(); - }); - - it('resets changed files before redirecting', () => { - jest.spyOn(eventHub, '$on').mockImplementation(); - - store.state.commit.commitAction = '3'; - - return store.dispatch('commit/commitChanges').then(() => { - expect(store.state.stagedFiles.length).toBe(0); - }); - }); - }); - }); - - describe('success response with failed message', () => { - beforeEach(() => { - jest.spyOn(service, 'commit').mockResolvedValue({ - data: { - message: 'failed message', - }, - }); - }); - - it('shows failed message', async () => { - await store.dispatch('commit/commitChanges'); - const alert = document.querySelector('.flash-container'); - - expect(alert.textContent.trim()).toBe('failed message'); - }); - }); - - describe('failed response', () => { - beforeEach(() => { - jest.spyOn(service, 'commit').mockRejectedValue({}); - }); - - it('commits error updates', async () => { - jest.spyOn(store, 'commit'); - - await store.dispatch('commit/commitChanges').catch(() => {}); - - expect(store.commit.mock.calls).toEqual([ - ['commit/CLEAR_ERROR', undefined, undefined], - ['commit/UPDATE_LOADING', true, undefined], - ['commit/UPDATE_LOADING', false, undefined], - ['commit/SET_ERROR', createUnexpectedCommitError(), undefined], - ]); - }); - }); - - describe('first commit of a branch', () => { - it('commits TOGGLE_EMPTY_STATE mutation on empty repo', async () => { - jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE }); - jest.spyOn(store, 'commit'); - - await store.dispatch('commit/commitChanges'); - expect(store.commit.mock.calls).toEqual( - expect.arrayContaining([['TOGGLE_EMPTY_STATE', expect.any(Object), expect.any(Object)]]), - ); - }); - - it('does not commmit TOGGLE_EMPTY_STATE mutation on existing project', async () => { - COMMIT_RESPONSE.parent_ids.push('1234'); - jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE }); - jest.spyOn(store, 'commit'); - - await store.dispatch('commit/commitChanges'); - expect(store.commit.mock.calls).not.toEqual( - expect.arrayContaining([['TOGGLE_EMPTY_STATE', expect.any(Object), expect.any(Object)]]), - ); - }); - }); - }); - - describe('toggleShouldCreateMR', () => { - it('commits both toggle and interacting with MR checkbox actions', () => { - return testAction( - actions.toggleShouldCreateMR, - {}, - store.state, - [{ type: mutationTypes.TOGGLE_SHOULD_CREATE_MR }], - [], - ); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/commit/getters_spec.js b/spec/frontend/ide/stores/modules/commit/getters_spec.js deleted file mode 100644 index 38ebe36c2c5..00000000000 --- a/spec/frontend/ide/stores/modules/commit/getters_spec.js +++ /dev/null @@ -1,305 +0,0 @@ -import { - COMMIT_TO_CURRENT_BRANCH, - COMMIT_TO_NEW_BRANCH, -} from '~/ide/stores/modules/commit/constants'; -import * as getters from '~/ide/stores/modules/commit/getters'; -import commitState from '~/ide/stores/modules/commit/state'; - -describe('IDE commit module getters', () => { - let state; - - beforeEach(() => { - state = commitState(); - }); - - describe('discardDraftButtonDisabled', () => { - it('returns true when commitMessage is empty', () => { - expect(getters.discardDraftButtonDisabled(state)).toBe(true); - }); - - it('returns false when commitMessage is not empty & loading is false', () => { - state.commitMessage = 'test'; - state.submitCommitLoading = false; - - expect(getters.discardDraftButtonDisabled(state)).toBe(false); - }); - - it('returns true when commitMessage is not empty & loading is true', () => { - state.commitMessage = 'test'; - state.submitCommitLoading = true; - - expect(getters.discardDraftButtonDisabled(state)).toBe(true); - }); - }); - - describe('placeholderBranchName', () => { - it('includes username, currentBranchId, patch & random number', () => { - gon.current_username = 'username'; - - const branch = getters.placeholderBranchName(state, null, { - currentBranchId: 'testing', - }); - - expect(branch).toMatch(/username-testing-patch-\d{5}$/); - }); - }); - - describe('branchName', () => { - const rootState = { - currentBranchId: 'main', - }; - const localGetters = { - placeholderBranchName: 'placeholder-branch-name', - }; - - beforeEach(() => { - Object.assign(state, { - newBranchName: 'state-newBranchName', - }); - }); - - it('defaults to currentBranchId when not committing to a new branch', () => { - localGetters.isCreatingNewBranch = false; - - expect(getters.branchName(state, localGetters, rootState)).toBe('main'); - }); - - describe('commit to a new branch', () => { - beforeEach(() => { - localGetters.isCreatingNewBranch = true; - }); - - it('uses newBranchName when not empty', () => { - const newBranchName = 'nonempty-branch-name'; - Object.assign(state, { - newBranchName, - }); - - expect(getters.branchName(state, localGetters, rootState)).toBe(newBranchName); - }); - - it('uses placeholderBranchName when state newBranchName is empty', () => { - Object.assign(state, { - newBranchName: '', - }); - - expect(getters.branchName(state, localGetters, rootState)).toBe('placeholder-branch-name'); - }); - }); - }); - - describe('preBuiltCommitMessage', () => { - let rootState = {}; - - beforeEach(() => { - rootState.changedFiles = []; - rootState.stagedFiles = []; - }); - - afterEach(() => { - rootState = {}; - }); - - it('returns commitMessage when set', () => { - state.commitMessage = 'test commit message'; - - expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe('test commit message'); - }); - - ['changedFiles', 'stagedFiles'].forEach((key) => { - it('returns commitMessage with updated file', () => { - rootState[key].push({ - path: 'test-file', - }); - - expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe('Update test-file'); - }); - - it('returns commitMessage with updated files', () => { - rootState[key].push( - { - path: 'test-file', - }, - { - path: 'index.js', - }, - ); - - expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe( - 'Update test-file, index.js', - ); - }); - - it('returns commitMessage with deleted files', () => { - rootState[key].push( - { - path: 'test-file', - deleted: true, - }, - { - path: 'index.js', - }, - ); - - expect(getters.preBuiltCommitMessage(state, null, rootState)).toBe( - 'Update index.js\nDeleted test-file', - ); - }); - }); - }); - - describe('isCreatingNewBranch', () => { - it('returns false if NOT creating a new branch', () => { - state.commitAction = COMMIT_TO_CURRENT_BRANCH; - - expect(getters.isCreatingNewBranch(state)).toBe(false); - }); - - it('returns true if creating a new branch', () => { - state.commitAction = COMMIT_TO_NEW_BRANCH; - - expect(getters.isCreatingNewBranch(state)).toBe(true); - }); - }); - - describe('shouldHideNewMrOption', () => { - let localGetters = {}; - let rootGetters = {}; - - beforeEach(() => { - localGetters = { - isCreatingNewBranch: null, - }; - rootGetters = { - isOnDefaultBranch: null, - hasMergeRequest: null, - canPushToBranch: null, - }; - }); - - describe('NO existing MR for the branch', () => { - beforeEach(() => { - rootGetters.hasMergeRequest = false; - }); - - it('should never hide "New MR" option', () => { - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull(); - }); - }); - - describe('existing MR for the branch', () => { - beforeEach(() => { - rootGetters.hasMergeRequest = true; - }); - - it('should NOT hide "New MR" option if user can NOT push to the current branch', () => { - rootGetters.canPushToBranch = false; - - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false); - }); - - it('should hide "New MR" option if user can push to the current branch', () => { - rootGetters.canPushToBranch = true; - - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true); - }); - }); - - describe('user can NOT push the branch', () => { - beforeEach(() => { - rootGetters.canPushToBranch = false; - }); - - it('should never hide "New MR" option', () => { - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull(); - }); - }); - - describe('user can push to the branch', () => { - beforeEach(() => { - rootGetters.canPushToBranch = true; - }); - - it('should NOT hide "New MR" option if there is NO existing MR for the current branch', () => { - rootGetters.hasMergeRequest = false; - - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBeNull(); - }); - - it('should hide "New MR" option if there is existing MR for the current branch', () => { - rootGetters.hasMergeRequest = true; - - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true); - }); - }); - - describe('default branch', () => { - beforeEach(() => { - rootGetters.isOnDefaultBranch = true; - }); - - describe('committing to the same branch', () => { - beforeEach(() => { - localGetters.isCreatingNewBranch = false; - rootGetters.canPushToBranch = true; - }); - - it('should hide "New MR" when there is an existing MR', () => { - rootGetters.hasMergeRequest = true; - - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true); - }); - - it('should hide "New MR" when there is no existing MR', () => { - rootGetters.hasMergeRequest = false; - - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(true); - }); - }); - - describe('creating a new branch', () => { - beforeEach(() => { - localGetters.isCreatingNewBranch = true; - }); - - it('should NOT hide "New MR" option no matter existence of an MR or write access', () => { - rootGetters.hasMergeRequest = false; - rootGetters.canPushToBranch = true; - - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false); - - rootGetters.hasMergeRequest = true; - rootGetters.canPushToBranch = true; - - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false); - - rootGetters.hasMergeRequest = false; - rootGetters.canPushToBranch = false; - - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false); - }); - }); - }); - - it('should never hide "New MR" option when creating a new branch', () => { - localGetters.isCreatingNewBranch = true; - - rootGetters.isOnDefaultBranch = false; - rootGetters.hasMergeRequest = true; - rootGetters.canPushToBranch = true; - - expect(getters.shouldHideNewMrOption(state, localGetters, null, rootGetters)).toBe(false); - }); - }); - - describe('shouldDisableNewMrOption', () => { - it.each` - rootGetters | expectedValue - ${{ canCreateMergeRequests: false, emptyRepo: false }} | ${true} - ${{ canCreateMergeRequests: true, emptyRepo: true }} | ${true} - ${{ canCreateMergeRequests: true, emptyRepo: false }} | ${false} - `('with $rootGetters, it is $expectedValue', ({ rootGetters, expectedValue }) => { - expect(getters.shouldDisableNewMrOption(state, getters, {}, rootGetters)).toBe(expectedValue); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js deleted file mode 100644 index d277157e737..00000000000 --- a/spec/frontend/ide/stores/modules/commit/mutations_spec.js +++ /dev/null @@ -1,86 +0,0 @@ -import * as types from '~/ide/stores/modules/commit/mutation_types'; -import mutations from '~/ide/stores/modules/commit/mutations'; -import commitState from '~/ide/stores/modules/commit/state'; - -describe('IDE commit module mutations', () => { - let state; - - beforeEach(() => { - state = commitState(); - }); - - describe('UPDATE_COMMIT_MESSAGE', () => { - it('updates commitMessage', () => { - mutations.UPDATE_COMMIT_MESSAGE(state, 'testing'); - - expect(state.commitMessage).toBe('testing'); - }); - }); - - describe('UPDATE_COMMIT_ACTION', () => { - it('updates commitAction', () => { - mutations.UPDATE_COMMIT_ACTION(state, { commitAction: 'testing' }); - - expect(state.commitAction).toBe('testing'); - }); - }); - - describe('UPDATE_NEW_BRANCH_NAME', () => { - it('updates newBranchName', () => { - mutations.UPDATE_NEW_BRANCH_NAME(state, 'testing'); - - expect(state.newBranchName).toBe('testing'); - }); - }); - - describe('UPDATE_LOADING', () => { - it('updates submitCommitLoading', () => { - mutations.UPDATE_LOADING(state, true); - - expect(state.submitCommitLoading).toBe(true); - }); - }); - - describe('TOGGLE_SHOULD_CREATE_MR', () => { - it('changes shouldCreateMR to true when initial state is false', () => { - state.shouldCreateMR = false; - mutations.TOGGLE_SHOULD_CREATE_MR(state); - - expect(state.shouldCreateMR).toBe(true); - }); - - it('changes shouldCreateMR to false when initial state is true', () => { - state.shouldCreateMR = true; - mutations.TOGGLE_SHOULD_CREATE_MR(state); - - expect(state.shouldCreateMR).toBe(false); - }); - - it('sets shouldCreateMR to given value when passed in', () => { - state.shouldCreateMR = false; - mutations.TOGGLE_SHOULD_CREATE_MR(state, false); - - expect(state.shouldCreateMR).toBe(false); - }); - }); - - describe(types.CLEAR_ERROR, () => { - it('should clear commitError', () => { - state.commitError = {}; - - mutations[types.CLEAR_ERROR](state); - - expect(state.commitError).toBeNull(); - }); - }); - - describe(types.SET_ERROR, () => { - it('should set commitError', () => { - const error = { title: 'foo' }; - - mutations[types.SET_ERROR](state, error); - - expect(state.commitError).toBe(error); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/editor/actions_spec.js b/spec/frontend/ide/stores/modules/editor/actions_spec.js deleted file mode 100644 index e24d54ef6da..00000000000 --- a/spec/frontend/ide/stores/modules/editor/actions_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import testAction from 'helpers/vuex_action_helper'; -import * as actions from '~/ide/stores/modules/editor/actions'; -import * as types from '~/ide/stores/modules/editor/mutation_types'; -import { createTriggerRenamePayload } from '../../../helpers'; - -describe('~/ide/stores/modules/editor/actions', () => { - describe('updateFileEditor', () => { - it('commits with payload', () => { - const payload = {}; - - return testAction(actions.updateFileEditor, payload, {}, [ - { type: types.UPDATE_FILE_EDITOR, payload }, - ]); - }); - }); - - describe('removeFileEditor', () => { - it('commits with payload', () => { - const payload = 'path/to/file.txt'; - - return testAction(actions.removeFileEditor, payload, {}, [ - { type: types.REMOVE_FILE_EDITOR, payload }, - ]); - }); - }); - - describe('renameFileEditor', () => { - it('commits with payload', () => { - const payload = createTriggerRenamePayload('test', 'test123'); - - return testAction(actions.renameFileEditor, payload, {}, [ - { type: types.RENAME_FILE_EDITOR, payload }, - ]); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/editor/getters_spec.js b/spec/frontend/ide/stores/modules/editor/getters_spec.js deleted file mode 100644 index 14099cdaeb2..00000000000 --- a/spec/frontend/ide/stores/modules/editor/getters_spec.js +++ /dev/null @@ -1,31 +0,0 @@ -import * as getters from '~/ide/stores/modules/editor/getters'; -import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils'; - -const TEST_PATH = 'test/path.md'; -const TEST_FILE_EDITOR = { - ...createDefaultFileEditor(), - editorRow: 7, - editorColumn: 8, - fileLanguage: 'markdown', -}; - -describe('~/ide/stores/modules/editor/getters', () => { - describe('activeFileEditor', () => { - it.each` - activeFile | fileEditors | expected - ${null} | ${{}} | ${null} - ${{}} | ${{}} | ${createDefaultFileEditor()} - ${{ path: TEST_PATH }} | ${{}} | ${createDefaultFileEditor()} - ${{ path: TEST_PATH }} | ${{ bogus: createDefaultFileEditor(), [TEST_PATH]: TEST_FILE_EDITOR }} | ${TEST_FILE_EDITOR} - `( - 'with activeFile=$activeFile and fileEditors=$fileEditors', - ({ activeFile, fileEditors, expected }) => { - const rootGetters = { activeFile }; - const state = { fileEditors }; - const result = getters.activeFileEditor(state, {}, {}, rootGetters); - - expect(result).toEqual(expected); - }, - ); - }); -}); diff --git a/spec/frontend/ide/stores/modules/editor/mutations_spec.js b/spec/frontend/ide/stores/modules/editor/mutations_spec.js deleted file mode 100644 index 35d13f375a3..00000000000 --- a/spec/frontend/ide/stores/modules/editor/mutations_spec.js +++ /dev/null @@ -1,78 +0,0 @@ -import * as types from '~/ide/stores/modules/editor/mutation_types'; -import mutations from '~/ide/stores/modules/editor/mutations'; -import { createDefaultFileEditor } from '~/ide/stores/modules/editor/utils'; -import { createTriggerRenamePayload } from '../../../helpers'; - -const TEST_PATH = 'test/path.md'; - -describe('~/ide/stores/modules/editor/mutations', () => { - describe(types.UPDATE_FILE_EDITOR, () => { - it('with path that does not exist, should initialize with default values', () => { - const state = { fileEditors: {} }; - const data = { fileLanguage: 'markdown' }; - - mutations[types.UPDATE_FILE_EDITOR](state, { path: TEST_PATH, data }); - - expect(state.fileEditors).toEqual({ - [TEST_PATH]: { - ...createDefaultFileEditor(), - ...data, - }, - }); - }); - - it('with existing path, should overwrite values', () => { - const state = { - fileEditors: { - foo: {}, - [TEST_PATH]: { ...createDefaultFileEditor(), editorRow: 7, editorColumn: 7 }, - }, - }; - - mutations[types.UPDATE_FILE_EDITOR](state, { - path: TEST_PATH, - data: { fileLanguage: 'markdown' }, - }); - - expect(state).toEqual({ - fileEditors: { - foo: {}, - [TEST_PATH]: { - ...createDefaultFileEditor(), - editorRow: 7, - editorColumn: 7, - fileLanguage: 'markdown', - }, - }, - }); - }); - }); - - describe(types.REMOVE_FILE_EDITOR, () => { - it.each` - fileEditors | path | expected - ${{}} | ${'does/not/exist.txt'} | ${{}} - ${{ foo: {}, [TEST_PATH]: {} }} | ${TEST_PATH} | ${{ foo: {} }} - `('removes file $path', ({ fileEditors, path, expected }) => { - const state = { fileEditors }; - - mutations[types.REMOVE_FILE_EDITOR](state, path); - - expect(state).toEqual({ fileEditors: expected }); - }); - }); - - describe(types.RENAME_FILE_EDITOR, () => { - it.each` - fileEditors | payload | expected - ${{ foo: {} }} | ${createTriggerRenamePayload('does/not/exist', 'abc')} | ${{ foo: {} }} - ${{ foo: { a: 1 }, bar: {} }} | ${createTriggerRenamePayload('foo', 'abc/def')} | ${{ 'abc/def': { a: 1 }, bar: {} }} - `('renames fileEditor at $payload', ({ fileEditors, payload, expected }) => { - const state = { fileEditors }; - - mutations[types.RENAME_FILE_EDITOR](state, payload); - - expect(state).toEqual({ fileEditors: expected }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/editor/setup_spec.js b/spec/frontend/ide/stores/modules/editor/setup_spec.js deleted file mode 100644 index df0dfb6f260..00000000000 --- a/spec/frontend/ide/stores/modules/editor/setup_spec.js +++ /dev/null @@ -1,54 +0,0 @@ -import { cloneDeep } from 'lodash'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import eventHub from '~/ide/eventhub'; -import { createStoreOptions } from '~/ide/stores'; -import { setupFileEditorsSync } from '~/ide/stores/modules/editor/setup'; -import { createTriggerRenamePayload, createTriggerUpdatePayload } from '../../../helpers'; - -describe('~/ide/stores/modules/editor/setup', () => { - let store; - - beforeEach(() => { - store = new Vuex.Store(createStoreOptions()); - store.state.entries = { - foo: {}, - bar: {}, - }; - store.state.editor.fileEditors = { - foo: {}, - bizz: {}, - }; - - setupFileEditorsSync(store); - }); - - it('when files change is emitted, removes unused fileEditors', () => { - eventHub.$emit('ide.files.change'); - - expect(store.state.entries).toEqual({ - foo: {}, - bar: {}, - }); - expect(store.state.editor.fileEditors).toEqual({ - foo: {}, - }); - }); - - it('when files update is emitted, does nothing', () => { - const origState = cloneDeep(store.state); - - eventHub.$emit('ide.files.change', createTriggerUpdatePayload('foo')); - - expect(store.state).toEqual(origState); - }); - - it('when files rename is emitted, renames fileEditor', () => { - eventHub.$emit('ide.files.change', createTriggerRenamePayload('foo', 'foo_new')); - - expect(store.state.editor.fileEditors).toEqual({ - foo_new: {}, - bizz: {}, - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/file_templates/actions_spec.js b/spec/frontend/ide/stores/modules/file_templates/actions_spec.js deleted file mode 100644 index a5ce507bd3c..00000000000 --- a/spec/frontend/ide/stores/modules/file_templates/actions_spec.js +++ /dev/null @@ -1,333 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; -import * as actions from '~/ide/stores/modules/file_templates/actions'; -import * as types from '~/ide/stores/modules/file_templates/mutation_types'; -import createState from '~/ide/stores/modules/file_templates/state'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; - -describe('IDE file templates actions', () => { - let state; - let mock; - - beforeEach(() => { - state = createState(); - - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('requestTemplateTypes', () => { - it('commits REQUEST_TEMPLATE_TYPES', () => { - return testAction( - actions.requestTemplateTypes, - null, - state, - [{ type: types.REQUEST_TEMPLATE_TYPES }], - [], - ); - }); - }); - - describe('receiveTemplateTypesError', () => { - it('commits RECEIVE_TEMPLATE_TYPES_ERROR and dispatches setErrorMessage', () => { - return testAction( - actions.receiveTemplateTypesError, - null, - state, - [{ type: types.RECEIVE_TEMPLATE_TYPES_ERROR }], - [ - { - type: 'setErrorMessage', - payload: { - action: expect.any(Function), - actionText: 'Please try again', - text: 'Error loading template types.', - }, - }, - ], - ); - }); - }); - - describe('receiveTemplateTypesSuccess', () => { - it('commits RECEIVE_TEMPLATE_TYPES_SUCCESS', () => { - return testAction( - actions.receiveTemplateTypesSuccess, - 'test', - state, - [{ type: types.RECEIVE_TEMPLATE_TYPES_SUCCESS, payload: 'test' }], - [], - ); - }); - }); - - describe('fetchTemplateTypes', () => { - describe('success', () => { - const pages = [[{ name: 'MIT' }], [{ name: 'Apache' }], [{ name: 'CC' }]]; - - beforeEach(() => { - mock.onGet(/api\/(.*)\/templates\/licenses/).reply(({ params }) => { - const pageNum = params.page; - const page = pages[pageNum - 1]; - const hasNextPage = pageNum < pages.length; - - return [HTTP_STATUS_OK, page, hasNextPage ? { 'X-NEXT-PAGE': pageNum + 1 } : {}]; - }); - }); - - it('rejects if selectedTemplateType is empty', async () => { - const dispatch = jest.fn().mockName('dispatch'); - - await expect(actions.fetchTemplateTypes({ dispatch, state })).rejects.toBeUndefined(); - expect(dispatch).not.toHaveBeenCalled(); - }); - - it('dispatches actions', () => { - state.selectedTemplateType = { key: 'licenses' }; - - return testAction( - actions.fetchTemplateTypes, - null, - state, - [], - [ - { type: 'requestTemplateTypes' }, - { type: 'receiveTemplateTypesSuccess', payload: pages[0] }, - { type: 'receiveTemplateTypesSuccess', payload: pages[0].concat(pages[1]) }, - { - type: 'receiveTemplateTypesSuccess', - payload: pages[0].concat(pages[1]).concat(pages[2]), - }, - ], - ); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - }); - - it('dispatches actions', () => { - state.selectedTemplateType = { key: 'licenses' }; - - return testAction( - actions.fetchTemplateTypes, - null, - state, - [], - [{ type: 'requestTemplateTypes' }, { type: 'receiveTemplateTypesError' }], - ); - }); - }); - }); - - describe('setSelectedTemplateType', () => { - it('commits SET_SELECTED_TEMPLATE_TYPE', () => { - const commit = jest.fn().mockName('commit'); - const options = { - commit, - dispatch() {}, - rootGetters: { activeFile: { name: 'test', prevPath: '' } }, - }; - - actions.setSelectedTemplateType(options, { name: 'test' }); - - expect(commit).toHaveBeenCalledWith(types.SET_SELECTED_TEMPLATE_TYPE, { name: 'test' }); - }); - - it('dispatches discardFileChanges if prevPath matches templates name', () => { - const dispatch = jest.fn().mockName('dispatch'); - const options = { - commit() {}, - - dispatch, - rootGetters: { activeFile: { name: 'test', path: 'test', prevPath: 'test' } }, - }; - - actions.setSelectedTemplateType(options, { name: 'test' }); - - expect(dispatch).toHaveBeenCalledWith('discardFileChanges', 'test', { root: true }); - }); - - it('dispatches renameEntry if file name doesnt match', () => { - const dispatch = jest.fn().mockName('dispatch'); - const options = { - commit() {}, - - dispatch, - rootGetters: { activeFile: { name: 'oldtest', path: 'oldtest', prevPath: '' } }, - }; - - actions.setSelectedTemplateType(options, { name: 'test' }); - - expect(dispatch).toHaveBeenCalledWith( - 'renameEntry', - { path: 'oldtest', name: 'test' }, - { root: true }, - ); - }); - }); - - describe('receiveTemplateError', () => { - it('dispatches setErrorMessage', () => { - return testAction( - actions.receiveTemplateError, - 'test', - state, - [], - [ - { - type: 'setErrorMessage', - payload: { - action: expect.any(Function), - actionText: 'Please try again', - text: 'Error loading template.', - actionPayload: 'test', - }, - }, - ], - ); - }); - }); - - describe('fetchTemplate', () => { - describe('success', () => { - beforeEach(() => { - mock - .onGet(/api\/(.*)\/templates\/licenses\/mit/) - .replyOnce(HTTP_STATUS_OK, { content: 'MIT content' }); - mock - .onGet(/api\/(.*)\/templates\/licenses\/testing/) - .replyOnce(HTTP_STATUS_OK, { content: 'testing content' }); - }); - - it('dispatches setFileTemplate if template already has content', () => { - const template = { content: 'already has content' }; - - return testAction( - actions.fetchTemplate, - template, - state, - [], - [{ type: 'setFileTemplate', payload: template }], - ); - }); - - it('dispatches success', () => { - const template = { key: 'mit' }; - - state.selectedTemplateType = { key: 'licenses' }; - - return testAction( - actions.fetchTemplate, - template, - state, - [], - [{ type: 'setFileTemplate', payload: { content: 'MIT content' } }], - ); - }); - - it('dispatches success and uses name key for API call', () => { - const template = { name: 'testing' }; - - state.selectedTemplateType = { key: 'licenses' }; - - return testAction( - actions.fetchTemplate, - template, - state, - [], - [{ type: 'setFileTemplate', payload: { content: 'testing content' } }], - ); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock - .onGet(/api\/(.*)\/templates\/licenses\/mit/) - .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - }); - - it('dispatches error', () => { - const template = { name: 'testing' }; - - state.selectedTemplateType = { key: 'licenses' }; - - return testAction( - actions.fetchTemplate, - template, - state, - [], - [{ type: 'receiveTemplateError', payload: template }], - ); - }); - }); - }); - - describe('setFileTemplate', () => { - it('dispatches changeFileContent', () => { - const dispatch = jest.fn().mockName('dispatch'); - const commit = jest.fn().mockName('commit'); - const rootGetters = { activeFile: { path: 'test' } }; - - actions.setFileTemplate({ dispatch, commit, rootGetters }, { content: 'content' }); - - expect(dispatch).toHaveBeenCalledWith( - 'changeFileContent', - { path: 'test', content: 'content' }, - { root: true }, - ); - }); - - it('commits SET_UPDATE_SUCCESS', () => { - const dispatch = jest.fn().mockName('dispatch'); - const commit = jest.fn().mockName('commit'); - const rootGetters = { activeFile: { path: 'test' } }; - - actions.setFileTemplate({ dispatch, commit, rootGetters }, { content: 'content' }); - - expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', true); - }); - }); - - describe('undoFileTemplate', () => { - it('dispatches changeFileContent', () => { - const dispatch = jest.fn().mockName('dispatch'); - const commit = jest.fn().mockName('commit'); - const rootGetters = { activeFile: { path: 'test', raw: 'raw content' } }; - - actions.undoFileTemplate({ dispatch, commit, rootGetters }); - - expect(dispatch).toHaveBeenCalledWith( - 'changeFileContent', - { path: 'test', content: 'raw content' }, - { root: true }, - ); - }); - - it('commits SET_UPDATE_SUCCESS', () => { - const dispatch = jest.fn().mockName('dispatch'); - const commit = jest.fn().mockName('commit'); - const rootGetters = { activeFile: { path: 'test', raw: 'raw content' } }; - - actions.undoFileTemplate({ dispatch, commit, rootGetters }); - - expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', false); - }); - - it('dispatches discardFileChanges if file has prevPath', () => { - const dispatch = jest.fn().mockName('dispatch'); - const rootGetters = { activeFile: { path: 'test', prevPath: 'newtest', raw: 'raw content' } }; - - actions.undoFileTemplate({ dispatch, commit() {}, rootGetters }); - - expect(dispatch).toHaveBeenCalledWith('discardFileChanges', 'test', { root: true }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/file_templates/getters_spec.js b/spec/frontend/ide/stores/modules/file_templates/getters_spec.js deleted file mode 100644 index 02e0d55346e..00000000000 --- a/spec/frontend/ide/stores/modules/file_templates/getters_spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import { leftSidebarViews } from '~/ide/constants'; -import * as getters from '~/ide/stores/modules/file_templates/getters'; -import createState from '~/ide/stores/state'; - -describe('IDE file templates getters', () => { - describe('templateTypes', () => { - it('returns list of template types', () => { - expect(getters.templateTypes().length).toBe(4); - }); - }); - - describe('showFileTemplatesBar', () => { - let rootState; - - beforeEach(() => { - rootState = createState(); - }); - - it('returns true if template is found and currentActivityView is edit', () => { - rootState.currentActivityView = leftSidebarViews.edit.name; - - expect( - getters.showFileTemplatesBar( - null, - { - templateTypes: getters.templateTypes(), - }, - rootState, - )('LICENSE'), - ).toBe(true); - }); - - it('returns false if template is found and currentActivityView is not edit', () => { - rootState.currentActivityView = leftSidebarViews.commit.name; - - expect( - getters.showFileTemplatesBar( - null, - { - templateTypes: getters.templateTypes(), - }, - rootState, - )('LICENSE'), - ).toBe(false); - }); - - it('returns undefined if not found', () => { - expect( - getters.showFileTemplatesBar( - null, - { - templateTypes: getters.templateTypes(), - }, - rootState, - )('test'), - ).toBe(undefined); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js deleted file mode 100644 index 3ea3c9507dd..00000000000 --- a/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js +++ /dev/null @@ -1,88 +0,0 @@ -import * as types from '~/ide/stores/modules/file_templates/mutation_types'; -import mutations from '~/ide/stores/modules/file_templates/mutations'; -import createState from '~/ide/stores/modules/file_templates/state'; - -const mockFileTemplates = [['MIT'], ['CC']]; -const mockTemplateType = 'test'; - -describe('IDE file templates mutations', () => { - let state; - - beforeEach(() => { - state = createState(); - }); - - describe(`${types.REQUEST_TEMPLATE_TYPES}`, () => { - it('sets loading to true', () => { - state.isLoading = false; - - mutations[types.REQUEST_TEMPLATE_TYPES](state); - - expect(state.isLoading).toBe(true); - }); - - it('sets templates to an empty array', () => { - state.templates = mockFileTemplates; - - mutations[types.REQUEST_TEMPLATE_TYPES](state); - - expect(state.templates).toEqual([]); - }); - }); - - describe(`${types.RECEIVE_TEMPLATE_TYPES_ERROR}`, () => { - it('sets isLoading', () => { - state.isLoading = true; - - mutations[types.RECEIVE_TEMPLATE_TYPES_ERROR](state); - - expect(state.isLoading).toBe(false); - }); - }); - - describe(`${types.RECEIVE_TEMPLATE_TYPES_SUCCESS}`, () => { - it('sets isLoading to false', () => { - state.isLoading = true; - - mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, mockFileTemplates); - - expect(state.isLoading).toBe(false); - }); - - it('sets templates to payload', () => { - state.templates = ['test']; - - mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, mockFileTemplates); - - expect(state.templates).toEqual(mockFileTemplates); - }); - }); - - describe(`${types.SET_SELECTED_TEMPLATE_TYPE}`, () => { - it('sets templates type to selected type', () => { - state.selectedTemplateType = ''; - - mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, mockTemplateType); - - expect(state.selectedTemplateType).toBe(mockTemplateType); - }); - - it('sets templates to empty array', () => { - state.templates = mockFileTemplates; - - mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, mockTemplateType); - - expect(state.templates).toEqual([]); - }); - }); - - describe(`${types.SET_UPDATE_SUCCESS}`, () => { - it('sets updateSuccess', () => { - state.updateSuccess = false; - - mutations[types.SET_UPDATE_SUCCESS](state, true); - - expect(state.updateSuccess).toBe(true); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js b/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js deleted file mode 100644 index 56901383f7b..00000000000 --- a/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js +++ /dev/null @@ -1,205 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; -import { - requestMergeRequests, - receiveMergeRequestsError, - receiveMergeRequestsSuccess, - fetchMergeRequests, - resetMergeRequests, -} from '~/ide/stores/modules/merge_requests/actions'; -import * as types from '~/ide/stores/modules/merge_requests/mutation_types'; -import state from '~/ide/stores/modules/merge_requests/state'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { mergeRequests } from '../../../mock_data'; - -describe('IDE merge requests actions', () => { - let mockedState; - let mockedRootState; - let mock; - - beforeEach(() => { - mockedState = state(); - mockedRootState = { currentProjectId: 7 }; - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('requestMergeRequests', () => { - it('should commit request', () => { - return testAction( - requestMergeRequests, - null, - mockedState, - [{ type: types.REQUEST_MERGE_REQUESTS }], - [], - ); - }); - }); - - describe('receiveMergeRequestsError', () => { - it('should commit error', () => { - return testAction( - receiveMergeRequestsError, - { type: 'created', search: '' }, - mockedState, - [{ type: types.RECEIVE_MERGE_REQUESTS_ERROR }], - [ - { - type: 'setErrorMessage', - payload: { - text: 'Error loading merge requests.', - action: expect.any(Function), - actionText: 'Please try again', - actionPayload: { type: 'created', search: '' }, - }, - }, - ], - ); - }); - }); - - describe('receiveMergeRequestsSuccess', () => { - it('should commit received data', () => { - return testAction( - receiveMergeRequestsSuccess, - mergeRequests, - mockedState, - [{ type: types.RECEIVE_MERGE_REQUESTS_SUCCESS, payload: mergeRequests }], - [], - ); - }); - }); - - describe('fetchMergeRequests', () => { - beforeEach(() => { - gon.api_version = 'v4'; - }); - - describe('success', () => { - beforeEach(() => { - mock.onGet(/\/api\/v4\/merge_requests\/?/).replyOnce(HTTP_STATUS_OK, mergeRequests); - }); - - it('calls API with params', () => { - const apiSpy = jest.spyOn(axios, 'get'); - fetchMergeRequests( - { - dispatch() {}, - - state: mockedState, - rootState: mockedRootState, - }, - { type: 'created' }, - ); - expect(apiSpy).toHaveBeenCalledWith(expect.anything(), { - params: { scope: 'created-by-me', state: 'opened', search: '' }, - }); - }); - - it('calls API with search', () => { - const apiSpy = jest.spyOn(axios, 'get'); - fetchMergeRequests( - { - dispatch() {}, - - state: mockedState, - rootState: mockedRootState, - }, - { type: 'created', search: 'testing search' }, - ); - expect(apiSpy).toHaveBeenCalledWith(expect.anything(), { - params: { scope: 'created-by-me', state: 'opened', search: 'testing search' }, - }); - }); - - it('dispatches success with received data', () => { - return testAction( - fetchMergeRequests, - { type: 'created' }, - mockedState, - [], - [ - { type: 'requestMergeRequests' }, - { type: 'resetMergeRequests' }, - { type: 'receiveMergeRequestsSuccess', payload: mergeRequests }, - ], - ); - }); - }); - - describe('success without type', () => { - beforeEach(() => { - mock - .onGet(/\/api\/v4\/projects\/.+\/merge_requests\/?$/) - .replyOnce(HTTP_STATUS_OK, mergeRequests); - }); - - it('calls API with project', () => { - const apiSpy = jest.spyOn(axios, 'get'); - fetchMergeRequests( - { - dispatch() {}, - - state: mockedState, - rootState: mockedRootState, - }, - { type: null, search: 'testing search' }, - ); - expect(apiSpy).toHaveBeenCalledWith( - expect.stringMatching(`projects/${mockedRootState.currentProjectId}/merge_requests`), - { params: { state: 'opened', search: 'testing search' } }, - ); - }); - - it('dispatches success with received data', () => { - return testAction( - fetchMergeRequests, - { type: null }, - { ...mockedState, ...mockedRootState }, - [], - [ - { type: 'requestMergeRequests' }, - { type: 'resetMergeRequests' }, - { type: 'receiveMergeRequestsSuccess', payload: mergeRequests }, - ], - ); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - }); - - it('dispatches error', () => { - return testAction( - fetchMergeRequests, - { type: 'created', search: '' }, - mockedState, - [], - [ - { type: 'requestMergeRequests' }, - { type: 'resetMergeRequests' }, - { type: 'receiveMergeRequestsError', payload: { type: 'created', search: '' } }, - ], - ); - }); - }); - }); - - describe('resetMergeRequests', () => { - it('commits reset', () => { - return testAction( - resetMergeRequests, - null, - mockedState, - [{ type: types.RESET_MERGE_REQUESTS }], - [], - ); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js b/spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js deleted file mode 100644 index f45c577f801..00000000000 --- a/spec/frontend/ide/stores/modules/merge_requests/mutations_spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import { TEST_HOST } from 'helpers/test_constants'; -import * as types from '~/ide/stores/modules/merge_requests/mutation_types'; -import mutations from '~/ide/stores/modules/merge_requests/mutations'; -import state from '~/ide/stores/modules/merge_requests/state'; -import { mergeRequests } from '../../../mock_data'; - -describe('IDE merge requests mutations', () => { - let mockedState; - - beforeEach(() => { - mockedState = state(); - }); - - describe('REQUEST_MERGE_REQUESTS', () => { - it('sets loading to true', () => { - mutations[types.REQUEST_MERGE_REQUESTS](mockedState); - - expect(mockedState.isLoading).toBe(true); - }); - }); - - describe('RECEIVE_MERGE_REQUESTS_ERROR', () => { - it('sets loading to false', () => { - mutations[types.RECEIVE_MERGE_REQUESTS_ERROR](mockedState); - - expect(mockedState.isLoading).toBe(false); - }); - }); - - describe('RECEIVE_MERGE_REQUESTS_SUCCESS', () => { - it('sets merge requests', () => { - gon.gitlab_url = TEST_HOST; - mutations[types.RECEIVE_MERGE_REQUESTS_SUCCESS](mockedState, mergeRequests); - - expect(mockedState.mergeRequests).toEqual([ - { - id: 1, - iid: 1, - title: 'Test merge request', - projectId: 1, - projectPathWithNamespace: 'namespace/project-path', - }, - ]); - }); - }); - - describe('RESET_MERGE_REQUESTS', () => { - it('clears merge request array', () => { - mockedState.mergeRequests = ['test']; - - mutations[types.RESET_MERGE_REQUESTS](mockedState); - - expect(mockedState.mergeRequests).toEqual([]); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/pane/actions_spec.js b/spec/frontend/ide/stores/modules/pane/actions_spec.js deleted file mode 100644 index 98c4f22dac8..00000000000 --- a/spec/frontend/ide/stores/modules/pane/actions_spec.js +++ /dev/null @@ -1,67 +0,0 @@ -import testAction from 'helpers/vuex_action_helper'; -import * as actions from '~/ide/stores/modules/pane/actions'; -import * as types from '~/ide/stores/modules/pane/mutation_types'; - -describe('IDE pane module actions', () => { - const TEST_VIEW = { name: 'test' }; - const TEST_VIEW_KEEP_ALIVE = { name: 'test-keep-alive', keepAlive: true }; - - describe('toggleOpen', () => { - it('dispatches open if closed', () => { - return testAction(actions.toggleOpen, TEST_VIEW, { isOpen: false }, [], [{ type: 'open' }]); - }); - - it('dispatches close if opened', () => { - return testAction(actions.toggleOpen, TEST_VIEW, { isOpen: true }, [], [{ type: 'close' }]); - }); - }); - - describe('open', () => { - describe('with a view specified', () => { - it('commits SET_OPEN and SET_CURRENT_VIEW', () => { - return testAction( - actions.open, - TEST_VIEW, - {}, - [ - { type: types.SET_OPEN, payload: true }, - { type: types.SET_CURRENT_VIEW, payload: TEST_VIEW.name }, - ], - [], - ); - }); - - it('commits KEEP_ALIVE_VIEW if keepAlive is true', () => { - return testAction( - actions.open, - TEST_VIEW_KEEP_ALIVE, - {}, - [ - { type: types.SET_OPEN, payload: true }, - { type: types.SET_CURRENT_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name }, - { type: types.KEEP_ALIVE_VIEW, payload: TEST_VIEW_KEEP_ALIVE.name }, - ], - [], - ); - }); - }); - - describe('without a view specified', () => { - it('commits SET_OPEN', () => { - return testAction( - actions.open, - undefined, - {}, - [{ type: types.SET_OPEN, payload: true }], - [], - ); - }); - }); - }); - - describe('close', () => { - it('commits SET_OPEN', () => { - return testAction(actions.close, null, {}, [{ type: types.SET_OPEN, payload: false }], []); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/pane/getters_spec.js b/spec/frontend/ide/stores/modules/pane/getters_spec.js deleted file mode 100644 index a321571f058..00000000000 --- a/spec/frontend/ide/stores/modules/pane/getters_spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import * as getters from '~/ide/stores/modules/pane/getters'; -import state from '~/ide/stores/modules/pane/state'; - -describe('IDE pane module getters', () => { - const TEST_VIEW = 'test-view'; - const TEST_KEEP_ALIVE_VIEWS = { - [TEST_VIEW]: true, - }; - - describe('isAliveView', () => { - it('returns true if given view is in keepAliveViews', () => { - const result = getters.isAliveView({ keepAliveViews: TEST_KEEP_ALIVE_VIEWS }, {})(TEST_VIEW); - - expect(result).toBe(true); - }); - - it('returns true if given view is active view and open', () => { - const result = getters.isAliveView({ ...state(), isOpen: true, currentView: TEST_VIEW })( - TEST_VIEW, - ); - - expect(result).toBe(true); - }); - - it('returns false if given view is active view and closed', () => { - const result = getters.isAliveView({ ...state(), currentView: TEST_VIEW })(TEST_VIEW); - - expect(result).toBe(false); - }); - - it('returns false if given view is not activeView', () => { - const result = getters.isAliveView({ - ...state(), - isOpen: true, - currentView: `${TEST_VIEW}_other`, - })(TEST_VIEW); - - expect(result).toBe(false); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/pane/mutations_spec.js b/spec/frontend/ide/stores/modules/pane/mutations_spec.js deleted file mode 100644 index eaeb2c8cd28..00000000000 --- a/spec/frontend/ide/stores/modules/pane/mutations_spec.js +++ /dev/null @@ -1,42 +0,0 @@ -import * as types from '~/ide/stores/modules/pane/mutation_types'; -import mutations from '~/ide/stores/modules/pane/mutations'; -import state from '~/ide/stores/modules/pane/state'; - -describe('IDE pane module mutations', () => { - const TEST_VIEW = 'test-view'; - let mockedState; - - beforeEach(() => { - mockedState = state(); - }); - - describe('SET_OPEN', () => { - it('sets isOpen', () => { - mockedState.isOpen = false; - - mutations[types.SET_OPEN](mockedState, true); - - expect(mockedState.isOpen).toBe(true); - }); - }); - - describe('SET_CURRENT_VIEW', () => { - it('sets currentView', () => { - mockedState.currentView = null; - - mutations[types.SET_CURRENT_VIEW](mockedState, TEST_VIEW); - - expect(mockedState.currentView).toEqual(TEST_VIEW); - }); - }); - - describe('KEEP_ALIVE_VIEW', () => { - it('adds entry to keepAliveViews', () => { - mutations[types.KEEP_ALIVE_VIEW](mockedState, TEST_VIEW); - - expect(mockedState.keepAliveViews).toEqual({ - [TEST_VIEW]: true, - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js deleted file mode 100644 index f49ff75ba7e..00000000000 --- a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js +++ /dev/null @@ -1,438 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import Visibility from 'visibilityjs'; -import { TEST_HOST } from 'helpers/test_constants'; -import testAction from 'helpers/vuex_action_helper'; -import { rightSidebarViews } from '~/ide/constants'; -import { - requestLatestPipeline, - receiveLatestPipelineError, - receiveLatestPipelineSuccess, - fetchLatestPipeline, - stopPipelinePolling, - clearEtagPoll, - requestJobs, - receiveJobsError, - receiveJobsSuccess, - fetchJobs, - toggleStageCollapsed, - setDetailJob, - requestJobLogs, - receiveJobLogsError, - receiveJobLogsSuccess, - fetchJobLogs, - resetLatestPipeline, -} from '~/ide/stores/modules/pipelines/actions'; -import * as types from '~/ide/stores/modules/pipelines/mutation_types'; -import state from '~/ide/stores/modules/pipelines/state'; -import axios from '~/lib/utils/axios_utils'; -import { - HTTP_STATUS_INTERNAL_SERVER_ERROR, - HTTP_STATUS_NOT_FOUND, - HTTP_STATUS_OK, -} from '~/lib/utils/http_status'; -import waitForPromises from 'helpers/wait_for_promises'; -import { pipelines, jobs } from '../../../mock_data'; - -describe('IDE pipelines actions', () => { - let mockedState; - let mock; - - beforeEach(() => { - mockedState = state(); - mock = new MockAdapter(axios); - - gon.api_version = 'v4'; - mockedState.currentProjectId = 'test/project'; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('requestLatestPipeline', () => { - it('commits request', () => { - return testAction( - requestLatestPipeline, - null, - mockedState, - [{ type: types.REQUEST_LATEST_PIPELINE }], - [], - ); - }); - }); - - describe('receiveLatestPipelineError', () => { - it('commits error', () => { - return testAction( - receiveLatestPipelineError, - { status: HTTP_STATUS_NOT_FOUND }, - mockedState, - [{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }], - [{ type: 'stopPipelinePolling' }], - ); - }); - - it('dispatches setErrorMessage is not 404', () => { - return testAction( - receiveLatestPipelineError, - { status: HTTP_STATUS_INTERNAL_SERVER_ERROR }, - mockedState, - [{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }], - [ - { - type: 'setErrorMessage', - payload: { - text: 'An error occurred while fetching the latest pipeline.', - action: expect.any(Function), - actionText: 'Please try again', - actionPayload: null, - }, - }, - { type: 'stopPipelinePolling' }, - ], - ); - }); - }); - - describe('receiveLatestPipelineSuccess', () => { - const rootGetters = { lastCommit: { id: '123' } }; - let commit; - - beforeEach(() => { - commit = jest.fn().mockName('commit'); - }); - - it('commits pipeline', () => { - receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines }); - expect(commit).toHaveBeenCalledWith(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipelines[0]); - }); - - it('commits false when there are no pipelines', () => { - receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines: [] }); - expect(commit).toHaveBeenCalledWith(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, false); - }); - }); - - describe('fetchLatestPipeline', () => { - afterEach(() => { - stopPipelinePolling(); - clearEtagPoll(); - }); - - describe('success', () => { - beforeEach(() => { - mock - .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines') - .reply(HTTP_STATUS_OK, { data: { foo: 'bar' } }, { 'poll-interval': '10000' }); - }); - - it('dispatches request', async () => { - jest.spyOn(axios, 'get'); - jest.spyOn(Visibility, 'hidden').mockReturnValue(false); - - const dispatch = jest.fn().mockName('dispatch'); - const rootGetters = { - lastCommit: { id: 'abc123def456ghi789jkl' }, - currentProject: { path_with_namespace: 'abc/def' }, - }; - - await fetchLatestPipeline({ dispatch, rootGetters }); - - expect(dispatch).toHaveBeenCalledWith('requestLatestPipeline'); - - await waitForPromises(); - - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledTimes(1); - expect(dispatch).toHaveBeenCalledWith('receiveLatestPipelineSuccess', expect.anything()); - - jest.advanceTimersByTime(10000); - - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledTimes(2); - expect(dispatch).toHaveBeenCalledWith('receiveLatestPipelineSuccess', expect.anything()); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock - .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines') - .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); - }); - - it('dispatches error', async () => { - const dispatch = jest.fn().mockName('dispatch'); - const rootGetters = { - lastCommit: { id: 'abc123def456ghi789jkl' }, - currentProject: { path_with_namespace: 'abc/def' }, - }; - - await fetchLatestPipeline({ dispatch, rootGetters }); - - await waitForPromises(); - - expect(dispatch).toHaveBeenCalledWith('receiveLatestPipelineError', expect.anything()); - }); - }); - - it('sets latest pipeline to `null` and stops polling on empty project', () => { - mockedState = { - ...mockedState, - rootGetters: { - lastCommit: null, - }, - }; - - return testAction( - fetchLatestPipeline, - {}, - mockedState, - [{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: null }], - [{ type: 'stopPipelinePolling' }], - ); - }); - }); - - describe('requestJobs', () => { - it('commits request', () => { - return testAction( - requestJobs, - 1, - mockedState, - [{ type: types.REQUEST_JOBS, payload: 1 }], - [], - ); - }); - }); - - describe('receiveJobsError', () => { - it('commits error', () => { - return testAction( - receiveJobsError, - { id: 1 }, - mockedState, - [{ type: types.RECEIVE_JOBS_ERROR, payload: 1 }], - [ - { - type: 'setErrorMessage', - payload: { - text: 'An error occurred while loading the pipelines jobs.', - action: expect.anything(), - actionText: 'Please try again', - actionPayload: { id: 1 }, - }, - }, - ], - ); - }); - }); - - describe('receiveJobsSuccess', () => { - it('commits data', () => { - return testAction( - receiveJobsSuccess, - { id: 1, data: jobs }, - mockedState, - [{ type: types.RECEIVE_JOBS_SUCCESS, payload: { id: 1, data: jobs } }], - [], - ); - }); - }); - - describe('fetchJobs', () => { - const stage = { id: 1, dropdownPath: `${TEST_HOST}/jobs` }; - - describe('success', () => { - beforeEach(() => { - mock.onGet(stage.dropdownPath).replyOnce(HTTP_STATUS_OK, jobs); - }); - - it('dispatches request', () => { - return testAction( - fetchJobs, - stage, - mockedState, - [], - [ - { type: 'requestJobs', payload: stage.id }, - { type: 'receiveJobsSuccess', payload: { id: stage.id, data: jobs } }, - ], - ); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock.onGet(stage.dropdownPath).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - }); - - it('dispatches error', () => { - return testAction( - fetchJobs, - stage, - mockedState, - [], - [ - { type: 'requestJobs', payload: stage.id }, - { type: 'receiveJobsError', payload: stage }, - ], - ); - }); - }); - }); - - describe('toggleStageCollapsed', () => { - it('commits collapse', () => { - return testAction( - toggleStageCollapsed, - 1, - mockedState, - [{ type: types.TOGGLE_STAGE_COLLAPSE, payload: 1 }], - [], - ); - }); - }); - - describe('setDetailJob', () => { - it('commits job', () => { - return testAction( - setDetailJob, - 'job', - mockedState, - [{ type: types.SET_DETAIL_JOB, payload: 'job' }], - [{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }], - ); - }); - - it('dispatches rightPane/open as pipeline when job is null', () => { - return testAction( - setDetailJob, - null, - mockedState, - [{ type: types.SET_DETAIL_JOB, payload: null }], - [{ type: 'rightPane/open', payload: rightSidebarViews.pipelines }], - ); - }); - - it('dispatches rightPane/open as job', () => { - return testAction( - setDetailJob, - 'job', - mockedState, - [{ type: types.SET_DETAIL_JOB, payload: 'job' }], - [{ type: 'rightPane/open', payload: rightSidebarViews.jobsDetail }], - ); - }); - }); - - describe('requestJobLogs', () => { - it('commits request', () => { - return testAction(requestJobLogs, null, mockedState, [{ type: types.REQUEST_JOB_LOGS }], []); - }); - }); - - describe('receiveJobLogsError', () => { - it('commits error', () => { - return testAction( - receiveJobLogsError, - null, - mockedState, - [{ type: types.RECEIVE_JOB_LOGS_ERROR }], - [ - { - type: 'setErrorMessage', - payload: { - text: 'An error occurred while fetching the job logs.', - action: expect.any(Function), - actionText: 'Please try again', - actionPayload: null, - }, - }, - ], - ); - }); - }); - - describe('receiveJobLogsSuccess', () => { - it('commits data', () => { - return testAction( - receiveJobLogsSuccess, - 'data', - mockedState, - [{ type: types.RECEIVE_JOB_LOGS_SUCCESS, payload: 'data' }], - [], - ); - }); - }); - - describe('fetchJobLogs', () => { - beforeEach(() => { - mockedState.detailJob = { path: `${TEST_HOST}/project/builds` }; - }); - - describe('success', () => { - beforeEach(() => { - jest.spyOn(axios, 'get'); - mock.onGet(`${TEST_HOST}/project/builds/trace`).replyOnce(HTTP_STATUS_OK, { html: 'html' }); - }); - - it('dispatches request', () => { - return testAction( - fetchJobLogs, - null, - mockedState, - [], - [ - { type: 'requestJobLogs' }, - { type: 'receiveJobLogsSuccess', payload: { html: 'html' } }, - ], - ); - }); - - it('sends get request to correct URL', () => { - fetchJobLogs({ - state: mockedState, - - dispatch() {}, - }); - expect(axios.get).toHaveBeenCalledWith(`${TEST_HOST}/project/builds/trace`, { - params: { format: 'json' }, - }); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock - .onGet(`${TEST_HOST}/project/builds/trace`) - .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); - }); - - it('dispatches error', () => { - return testAction( - fetchJobLogs, - null, - mockedState, - [], - [{ type: 'requestJobLogs' }, { type: 'receiveJobLogsError' }], - ); - }); - }); - }); - - describe('resetLatestPipeline', () => { - it('commits reset mutations', () => { - return testAction( - resetLatestPipeline, - null, - mockedState, - [ - { type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: null }, - { type: types.SET_DETAIL_JOB, payload: null }, - ], - [], - ); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/pipelines/getters_spec.js b/spec/frontend/ide/stores/modules/pipelines/getters_spec.js deleted file mode 100644 index 4514896b5ea..00000000000 --- a/spec/frontend/ide/stores/modules/pipelines/getters_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import * as getters from '~/ide/stores/modules/pipelines/getters'; -import state from '~/ide/stores/modules/pipelines/state'; - -describe('IDE pipeline getters', () => { - let mockedState; - - beforeEach(() => { - mockedState = state(); - }); - - describe('hasLatestPipeline', () => { - it('returns false when loading is true', () => { - mockedState.isLoadingPipeline = true; - - expect(getters.hasLatestPipeline(mockedState)).toBe(false); - }); - - it('returns false when pipelines is null', () => { - mockedState.latestPipeline = null; - - expect(getters.hasLatestPipeline(mockedState)).toBe(false); - }); - - it('returns false when loading is true & pipelines is null', () => { - mockedState.latestPipeline = null; - mockedState.isLoadingPipeline = true; - - expect(getters.hasLatestPipeline(mockedState)).toBe(false); - }); - - it('returns true when loading is false & pipelines is an object', () => { - mockedState.latestPipeline = { - id: 1, - }; - mockedState.isLoadingPipeline = false; - - expect(getters.hasLatestPipeline(mockedState)).toBe(true); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js b/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js deleted file mode 100644 index 0e738b98918..00000000000 --- a/spec/frontend/ide/stores/modules/pipelines/mutations_spec.js +++ /dev/null @@ -1,213 +0,0 @@ -import * as types from '~/ide/stores/modules/pipelines/mutation_types'; -import mutations from '~/ide/stores/modules/pipelines/mutations'; -import state from '~/ide/stores/modules/pipelines/state'; -import { fullPipelinesResponse, stages, jobs } from '../../../mock_data'; - -describe('IDE pipelines mutations', () => { - let mockedState; - - beforeEach(() => { - mockedState = state(); - }); - - describe('REQUEST_LATEST_PIPELINE', () => { - it('sets loading to true', () => { - mutations[types.REQUEST_LATEST_PIPELINE](mockedState); - - expect(mockedState.isLoadingPipeline).toBe(true); - }); - }); - - describe('RECEIVE_LASTEST_PIPELINE_ERROR', () => { - it('sets loading to false', () => { - mutations[types.RECEIVE_LASTEST_PIPELINE_ERROR](mockedState); - - expect(mockedState.isLoadingPipeline).toBe(false); - }); - }); - - describe('RECEIVE_LASTEST_PIPELINE_SUCCESS', () => { - const itSetsPipelineLoadingStates = () => { - it('sets has loaded to true', () => { - expect(mockedState.hasLoadedPipeline).toBe(true); - }); - - it('sets loading to false on success', () => { - expect(mockedState.isLoadingPipeline).toBe(false); - }); - }; - - describe('with pipeline', () => { - beforeEach(() => { - mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS]( - mockedState, - fullPipelinesResponse.data.pipelines[0], - ); - }); - - itSetsPipelineLoadingStates(); - - it('sets latestPipeline', () => { - expect(mockedState.latestPipeline).toEqual({ - id: '51', - path: 'test', - commit: { id: '123' }, - details: { status: expect.any(Object) }, - yamlError: undefined, - }); - }); - - it('sets stages', () => { - expect(mockedState.stages.length).toBe(2); - expect(mockedState.stages).toEqual([ - { - id: 0, - dropdownPath: stages[0].dropdown_path, - name: stages[0].name, - status: stages[0].status, - isCollapsed: false, - isLoading: false, - jobs: [], - }, - { - id: 1, - dropdownPath: stages[1].dropdown_path, - name: stages[1].name, - status: stages[1].status, - isCollapsed: false, - isLoading: false, - jobs: [], - }, - ]); - }); - }); - - describe('with null', () => { - beforeEach(() => { - mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null); - }); - - itSetsPipelineLoadingStates(); - - it('does not set latest pipeline if pipeline is null', () => { - expect(mockedState.latestPipeline).toEqual(null); - }); - }); - }); - - describe('REQUEST_JOBS', () => { - beforeEach(() => { - mockedState.stages = stages.map((stage, i) => ({ ...stage, id: i })); - }); - - it('sets isLoading on stage', () => { - mutations[types.REQUEST_JOBS](mockedState, mockedState.stages[0].id); - - expect(mockedState.stages[0].isLoading).toBe(true); - }); - }); - - describe('RECEIVE_JOBS_ERROR', () => { - beforeEach(() => { - mockedState.stages = stages.map((stage, i) => ({ ...stage, id: i })); - }); - - it('sets isLoading on stage after error', () => { - mutations[types.RECEIVE_JOBS_ERROR](mockedState, mockedState.stages[0].id); - - expect(mockedState.stages[0].isLoading).toBe(false); - }); - }); - - describe('RECEIVE_JOBS_SUCCESS', () => { - let data; - - beforeEach(() => { - mockedState.stages = stages.map((stage, i) => ({ ...stage, id: i })); - - data = { latest_statuses: [...jobs] }; - }); - - it('updates loading', () => { - mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data }); - expect(mockedState.stages[0].isLoading).toBe(false); - }); - - it('sets jobs on stage', () => { - mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data }); - expect(mockedState.stages[0].jobs.length).toBe(jobs.length); - expect(mockedState.stages[0].jobs).toEqual( - jobs.map((job) => ({ - id: job.id, - name: job.name, - status: job.status, - path: job.build_path, - rawPath: `${job.build_path}/raw`, - started: job.started, - isLoading: false, - output: '', - })), - ); - }); - }); - - describe('TOGGLE_STAGE_COLLAPSE', () => { - beforeEach(() => { - mockedState.stages = stages.map((stage, i) => ({ ...stage, id: i, isCollapsed: false })); - }); - - it('toggles collapsed state', () => { - mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id); - - expect(mockedState.stages[0].isCollapsed).toBe(true); - - mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id); - - expect(mockedState.stages[0].isCollapsed).toBe(false); - }); - }); - - describe('SET_DETAIL_JOB', () => { - it('sets detail job', () => { - mutations[types.SET_DETAIL_JOB](mockedState, jobs[0]); - - expect(mockedState.detailJob).toEqual(jobs[0]); - }); - }); - - describe('REQUEST_JOB_LOGS', () => { - beforeEach(() => { - mockedState.detailJob = { ...jobs[0] }; - }); - - it('sets loading on detail job', () => { - mutations[types.REQUEST_JOB_LOGS](mockedState); - - expect(mockedState.detailJob.isLoading).toBe(true); - }); - }); - - describe('RECEIVE_JOB_LOGS_ERROR', () => { - beforeEach(() => { - mockedState.detailJob = { ...jobs[0], isLoading: true }; - }); - - it('sets loading to false on detail job', () => { - mutations[types.RECEIVE_JOB_LOGS_ERROR](mockedState); - - expect(mockedState.detailJob.isLoading).toBe(false); - }); - }); - - describe('RECEIVE_JOB_LOGS_SUCCESS', () => { - beforeEach(() => { - mockedState.detailJob = { ...jobs[0], isLoading: true }; - }); - - it('sets output on detail job', () => { - mutations[types.RECEIVE_JOB_LOGS_SUCCESS](mockedState, { html: 'html' }); - expect(mockedState.detailJob.output).toBe('html'); - expect(mockedState.detailJob.isLoading).toBe(false); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/router/actions_spec.js b/spec/frontend/ide/stores/modules/router/actions_spec.js deleted file mode 100644 index 1458a43da57..00000000000 --- a/spec/frontend/ide/stores/modules/router/actions_spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import testAction from 'helpers/vuex_action_helper'; -import * as actions from '~/ide/stores/modules/router/actions'; -import * as types from '~/ide/stores/modules/router/mutation_types'; - -const TEST_PATH = 'test/path/abc'; - -describe('ide/stores/modules/router/actions', () => { - describe('push', () => { - it('commits mutation', () => { - return testAction( - actions.push, - TEST_PATH, - {}, - [{ type: types.PUSH, payload: TEST_PATH }], - [], - ); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/router/mutations_spec.js b/spec/frontend/ide/stores/modules/router/mutations_spec.js deleted file mode 100644 index 5a9f266db94..00000000000 --- a/spec/frontend/ide/stores/modules/router/mutations_spec.js +++ /dev/null @@ -1,23 +0,0 @@ -import * as types from '~/ide/stores/modules/router/mutation_types'; -import mutations from '~/ide/stores/modules/router/mutations'; -import createState from '~/ide/stores/modules/router/state'; - -const TEST_PATH = 'test/path/abc'; - -describe('ide/stores/modules/router/mutations', () => { - let state; - - beforeEach(() => { - state = createState(); - }); - - describe(types.PUSH, () => { - it('updates state', () => { - expect(state.fullPath).toBe(''); - - mutations[types.PUSH](state, TEST_PATH); - - expect(state.fullPath).toBe(TEST_PATH); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js deleted file mode 100644 index 8d8afda7014..00000000000 --- a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js +++ /dev/null @@ -1,302 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; -import { TEST_HOST } from 'spec/test_constants'; -import * as actions from '~/ide/stores/modules/terminal/actions/checks'; -import { - CHECK_CONFIG, - CHECK_RUNNERS, - RETRY_RUNNERS_INTERVAL, -} from '~/ide/stores/modules/terminal/constants'; -import * as messages from '~/ide/stores/modules/terminal/messages'; -import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types'; -import axios from '~/lib/utils/axios_utils'; -import { - HTTP_STATUS_BAD_REQUEST, - HTTP_STATUS_FORBIDDEN, - HTTP_STATUS_INTERNAL_SERVER_ERROR, - HTTP_STATUS_NOT_FOUND, - HTTP_STATUS_OK, - HTTP_STATUS_UNPROCESSABLE_ENTITY, -} from '~/lib/utils/http_status'; - -const TEST_PROJECT_PATH = 'lorem/root'; -const TEST_BRANCH_ID = 'main'; -const TEST_YAML_HELP_PATH = `${TEST_HOST}/test/yaml/help`; -const TEST_RUNNERS_HELP_PATH = `${TEST_HOST}/test/runners/help`; - -describe('IDE store terminal check actions', () => { - let mock; - let state; - let rootState; - let rootGetters; - - beforeEach(() => { - mock = new MockAdapter(axios); - state = { - paths: { - webTerminalConfigHelpPath: TEST_YAML_HELP_PATH, - webTerminalRunnersHelpPath: TEST_RUNNERS_HELP_PATH, - }, - checks: { - config: { isLoading: true }, - }, - }; - rootState = { - currentBranchId: TEST_BRANCH_ID, - }; - rootGetters = { - currentProject: { - id: 7, - path_with_namespace: TEST_PROJECT_PATH, - }, - }; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('requestConfigCheck', () => { - it('handles request loading', () => { - return testAction( - actions.requestConfigCheck, - null, - {}, - [{ type: mutationTypes.REQUEST_CHECK, payload: CHECK_CONFIG }], - [], - ); - }); - }); - - describe('receiveConfigCheckSuccess', () => { - it('handles successful response', () => { - return testAction( - actions.receiveConfigCheckSuccess, - null, - {}, - [ - { type: mutationTypes.SET_VISIBLE, payload: true }, - { type: mutationTypes.RECEIVE_CHECK_SUCCESS, payload: CHECK_CONFIG }, - ], - [], - ); - }); - }); - - describe('receiveConfigCheckError', () => { - it('handles error response', () => { - const status = HTTP_STATUS_UNPROCESSABLE_ENTITY; - const payload = { response: { status } }; - - return testAction( - actions.receiveConfigCheckError, - payload, - state, - [ - { - type: mutationTypes.SET_VISIBLE, - payload: true, - }, - { - type: mutationTypes.RECEIVE_CHECK_ERROR, - payload: { - type: CHECK_CONFIG, - message: messages.configCheckError(status, TEST_YAML_HELP_PATH), - }, - }, - ], - [], - ); - }); - - [HTTP_STATUS_FORBIDDEN, HTTP_STATUS_NOT_FOUND].forEach((status) => { - it(`hides tab, when status is ${status}`, () => { - const payload = { response: { status } }; - - return testAction( - actions.receiveConfigCheckError, - payload, - state, - [ - { - type: mutationTypes.SET_VISIBLE, - payload: false, - }, - expect.objectContaining({ type: mutationTypes.RECEIVE_CHECK_ERROR }), - ], - [], - ); - }); - }); - }); - - describe('fetchConfigCheck', () => { - it('dispatches request and receive', () => { - mock.onPost(/.*\/ide_terminals\/check_config/).reply(HTTP_STATUS_OK, {}); - - return testAction( - actions.fetchConfigCheck, - null, - { - ...rootGetters, - ...rootState, - }, - [], - [{ type: 'requestConfigCheck' }, { type: 'receiveConfigCheckSuccess' }], - ); - }); - - it('when error, dispatches request and receive', () => { - mock.onPost(/.*\/ide_terminals\/check_config/).reply(HTTP_STATUS_BAD_REQUEST, {}); - - return testAction( - actions.fetchConfigCheck, - null, - { - ...rootGetters, - ...rootState, - }, - [], - [ - { type: 'requestConfigCheck' }, - { type: 'receiveConfigCheckError', payload: expect.any(Error) }, - ], - ); - }); - }); - - describe('requestRunnersCheck', () => { - it('handles request loading', () => { - return testAction( - actions.requestRunnersCheck, - null, - {}, - [{ type: mutationTypes.REQUEST_CHECK, payload: CHECK_RUNNERS }], - [], - ); - }); - }); - - describe('receiveRunnersCheckSuccess', () => { - it('handles successful response, with data', () => { - const payload = [{}]; - - return testAction( - actions.receiveRunnersCheckSuccess, - payload, - state, - [{ type: mutationTypes.RECEIVE_CHECK_SUCCESS, payload: CHECK_RUNNERS }], - [], - ); - }); - - it('handles successful response, with empty data', () => { - const commitPayload = { - type: CHECK_RUNNERS, - message: messages.runnersCheckEmpty(TEST_RUNNERS_HELP_PATH), - }; - - return testAction( - actions.receiveRunnersCheckSuccess, - [], - state, - [{ type: mutationTypes.RECEIVE_CHECK_ERROR, payload: commitPayload }], - [{ type: 'retryRunnersCheck' }], - ); - }); - }); - - describe('receiveRunnersCheckError', () => { - it('dispatches handle with message', () => { - const commitPayload = { - type: CHECK_RUNNERS, - message: messages.UNEXPECTED_ERROR_RUNNERS, - }; - - return testAction( - actions.receiveRunnersCheckError, - null, - {}, - [{ type: mutationTypes.RECEIVE_CHECK_ERROR, payload: commitPayload }], - [], - ); - }); - }); - - describe('retryRunnersCheck', () => { - it('dispatches fetch again after timeout', () => { - const dispatch = jest.fn().mockName('dispatch'); - - actions.retryRunnersCheck({ dispatch, state }); - - expect(dispatch).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(RETRY_RUNNERS_INTERVAL + 1); - - expect(dispatch).toHaveBeenCalledWith('fetchRunnersCheck', { background: true }); - }); - - it('does not dispatch fetch if config check is error', () => { - const dispatch = jest.fn().mockName('dispatch'); - state.checks.config = { - isLoading: false, - isValid: false, - }; - - actions.retryRunnersCheck({ dispatch, state }); - - expect(dispatch).not.toHaveBeenCalled(); - - jest.advanceTimersByTime(RETRY_RUNNERS_INTERVAL + 1); - - expect(dispatch).not.toHaveBeenCalled(); - }); - }); - - describe('fetchRunnersCheck', () => { - it('dispatches request and receive', () => { - mock - .onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }) - .reply(HTTP_STATUS_OK, []); - - return testAction( - actions.fetchRunnersCheck, - {}, - rootGetters, - [], - [{ type: 'requestRunnersCheck' }, { type: 'receiveRunnersCheckSuccess', payload: [] }], - ); - }); - - it('does not dispatch request when background is true', () => { - mock - .onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }) - .reply(HTTP_STATUS_OK, []); - - return testAction( - actions.fetchRunnersCheck, - { background: true }, - rootGetters, - [], - [{ type: 'receiveRunnersCheckSuccess', payload: [] }], - ); - }); - - it('dispatches request and receive, when error', () => { - mock - .onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }) - .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, []); - - return testAction( - actions.fetchRunnersCheck, - {}, - rootGetters, - [], - [ - { type: 'requestRunnersCheck' }, - { type: 'receiveRunnersCheckError', payload: expect.any(Error) }, - ], - ); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js deleted file mode 100644 index 3f7ded5e718..00000000000 --- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js +++ /dev/null @@ -1,309 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/alert'; -import * as actions from '~/ide/stores/modules/terminal/actions/session_controls'; -import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants'; -import * as messages from '~/ide/stores/modules/terminal/messages'; -import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types'; -import axios from '~/lib/utils/axios_utils'; -import { - HTTP_STATUS_BAD_REQUEST, - HTTP_STATUS_NOT_FOUND, - HTTP_STATUS_OK, - HTTP_STATUS_UNPROCESSABLE_ENTITY, -} from '~/lib/utils/http_status'; - -jest.mock('~/alert'); - -const TEST_PROJECT_PATH = 'lorem/root'; -const TEST_BRANCH_ID = 'main'; -const TEST_SESSION = { - id: 7, - status: PENDING, - show_path: 'path/show', - cancel_path: 'path/cancel', - retry_path: 'path/retry', - terminal_path: 'path/terminal', - proxy_websocket_path: 'path/proxy', - services: ['test-service'], -}; - -describe('IDE store terminal session controls actions', () => { - let mock; - let dispatch; - let rootState; - let rootGetters; - - beforeEach(() => { - mock = new MockAdapter(axios); - dispatch = jest.fn().mockName('dispatch'); - rootState = { - currentBranchId: TEST_BRANCH_ID, - }; - rootGetters = { - currentProject: { - id: 7, - path_with_namespace: TEST_PROJECT_PATH, - }, - }; - }); - - afterEach(() => { - mock.restore(); - }); - - describe('requestStartSession', () => { - it('sets session status', () => { - return testAction( - actions.requestStartSession, - null, - {}, - [{ type: mutationTypes.SET_SESSION_STATUS, payload: STARTING }], - [], - ); - }); - }); - - describe('receiveStartSessionSuccess', () => { - it('sets session and starts polling status', () => { - return testAction( - actions.receiveStartSessionSuccess, - TEST_SESSION, - {}, - [ - { - type: mutationTypes.SET_SESSION, - payload: { - id: TEST_SESSION.id, - status: TEST_SESSION.status, - showPath: TEST_SESSION.show_path, - cancelPath: TEST_SESSION.cancel_path, - retryPath: TEST_SESSION.retry_path, - terminalPath: TEST_SESSION.terminal_path, - proxyWebsocketPath: TEST_SESSION.proxy_websocket_path, - services: TEST_SESSION.services, - }, - }, - ], - [{ type: 'pollSessionStatus' }], - ); - }); - }); - - describe('receiveStartSessionError', () => { - it('shows an alert', () => { - actions.receiveStartSessionError({ dispatch }); - - expect(createAlert).toHaveBeenCalledWith({ - message: messages.UNEXPECTED_ERROR_STARTING, - }); - }); - - it('sets session status', () => { - return testAction(actions.receiveStartSessionError, null, {}, [], [{ type: 'killSession' }]); - }); - }); - - describe('startSession', () => { - it('does nothing if session is already starting', () => { - const state = { - session: { status: STARTING }, - }; - - actions.startSession({ state, dispatch }); - - expect(dispatch).not.toHaveBeenCalled(); - }); - - it('dispatches request and receive on success', () => { - mock.onPost(/.*\/ide_terminals/).reply(HTTP_STATUS_OK, TEST_SESSION); - - return testAction( - actions.startSession, - null, - { ...rootGetters, ...rootState }, - [], - [ - { type: 'requestStartSession' }, - { type: 'receiveStartSessionSuccess', payload: TEST_SESSION }, - ], - ); - }); - - it('dispatches request and receive on error', () => { - mock.onPost(/.*\/ide_terminals/).reply(HTTP_STATUS_BAD_REQUEST); - - return testAction( - actions.startSession, - null, - { ...rootGetters, ...rootState }, - [], - [ - { type: 'requestStartSession' }, - { type: 'receiveStartSessionError', payload: expect.any(Error) }, - ], - ); - }); - }); - - describe('requestStopSession', () => { - it('sets session status', () => { - return testAction( - actions.requestStopSession, - null, - {}, - [{ type: mutationTypes.SET_SESSION_STATUS, payload: STOPPING }], - [], - ); - }); - }); - - describe('receiveStopSessionSuccess', () => { - it('kills the session', () => { - return testAction(actions.receiveStopSessionSuccess, null, {}, [], [{ type: 'killSession' }]); - }); - }); - - describe('receiveStopSessionError', () => { - it('shows an alert', () => { - actions.receiveStopSessionError({ dispatch }); - - expect(createAlert).toHaveBeenCalledWith({ - message: messages.UNEXPECTED_ERROR_STOPPING, - }); - }); - - it('kills the session', () => { - return testAction(actions.receiveStopSessionError, null, {}, [], [{ type: 'killSession' }]); - }); - }); - - describe('stopSession', () => { - it('dispatches request and receive on success', () => { - mock.onPost(TEST_SESSION.cancel_path).reply(HTTP_STATUS_OK, {}); - - const state = { - session: { cancelPath: TEST_SESSION.cancel_path }, - }; - - return testAction( - actions.stopSession, - null, - state, - [], - [{ type: 'requestStopSession' }, { type: 'receiveStopSessionSuccess' }], - ); - }); - - it('dispatches request and receive on error', () => { - mock.onPost(TEST_SESSION.cancel_path).reply(HTTP_STATUS_BAD_REQUEST); - - const state = { - session: { cancelPath: TEST_SESSION.cancel_path }, - }; - - return testAction( - actions.stopSession, - null, - state, - [], - [ - { type: 'requestStopSession' }, - { type: 'receiveStopSessionError', payload: expect.any(Error) }, - ], - ); - }); - }); - - describe('killSession', () => { - it('stops polling and sets status', () => { - return testAction( - actions.killSession, - null, - {}, - [{ type: mutationTypes.SET_SESSION_STATUS, payload: STOPPED }], - [{ type: 'stopPollingSessionStatus' }], - ); - }); - }); - - describe('restartSession', () => { - let state; - - beforeEach(() => { - state = { - session: { status: STOPPED, retryPath: 'test/retry' }, - }; - }); - - it('does nothing if current not stopped', () => { - state.session.status = STOPPING; - - actions.restartSession({ state, dispatch, rootState }); - - expect(dispatch).not.toHaveBeenCalled(); - }); - - it('dispatches startSession if retryPath is empty', () => { - state.session.retryPath = ''; - - return testAction( - actions.restartSession, - null, - { ...state, ...rootState }, - [], - [{ type: 'startSession' }], - ); - }); - - it('dispatches request and receive on success', () => { - mock - .onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' }) - .reply(HTTP_STATUS_OK, TEST_SESSION); - - return testAction( - actions.restartSession, - null, - { ...state, ...rootState }, - [], - [ - { type: 'requestStartSession' }, - { type: 'receiveStartSessionSuccess', payload: TEST_SESSION }, - ], - ); - }); - - it('dispatches request and receive on error', () => { - mock - .onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' }) - .reply(HTTP_STATUS_BAD_REQUEST); - - return testAction( - actions.restartSession, - null, - { ...state, ...rootState }, - [], - [ - { type: 'requestStartSession' }, - { type: 'receiveStartSessionError', payload: expect.any(Error) }, - ], - ); - }); - - [HTTP_STATUS_NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY].forEach((status) => { - it(`dispatches request and startSession on ${status}`, () => { - mock - .onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' }) - .reply(status); - - return testAction( - actions.restartSession, - null, - { ...state, ...rootState }, - [], - [{ type: 'requestStartSession' }, { type: 'startSession' }], - ); - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js deleted file mode 100644 index 30ae7d203a9..00000000000 --- a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js +++ /dev/null @@ -1,172 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import testAction from 'helpers/vuex_action_helper'; -import { createAlert } from '~/alert'; -import * as actions from '~/ide/stores/modules/terminal/actions/session_status'; -import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/terminal/constants'; -import * as messages from '~/ide/stores/modules/terminal/messages'; -import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types'; -import axios from '~/lib/utils/axios_utils'; -import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; - -jest.mock('~/alert'); - -const TEST_SESSION = { - id: 7, - status: PENDING, - show_path: 'path/show', - cancel_path: 'path/cancel', - retry_path: 'path/retry', - terminal_path: 'path/terminal', -}; - -describe('IDE store terminal session controls actions', () => { - let mock; - let dispatch; - let commit; - - beforeEach(() => { - mock = new MockAdapter(axios); - dispatch = jest.fn().mockName('dispatch'); - commit = jest.fn().mockName('commit'); - }); - - afterEach(() => { - mock.restore(); - }); - - describe('pollSessionStatus', () => { - it('starts interval to poll status', () => { - return testAction( - actions.pollSessionStatus, - null, - {}, - [{ type: mutationTypes.SET_SESSION_STATUS_INTERVAL, payload: expect.any(Number) }], - [{ type: 'stopPollingSessionStatus' }, { type: 'fetchSessionStatus' }], - ); - }); - - it('on interval, stops polling if no session', () => { - const state = { - session: null, - }; - - actions.pollSessionStatus({ state, dispatch, commit }); - dispatch.mockClear(); - - jest.advanceTimersByTime(5001); - - expect(dispatch).toHaveBeenCalledWith('stopPollingSessionStatus'); - }); - - it('on interval, fetches status', () => { - const state = { - session: TEST_SESSION, - }; - - actions.pollSessionStatus({ state, dispatch, commit }); - dispatch.mockClear(); - - jest.advanceTimersByTime(5001); - - expect(dispatch).toHaveBeenCalledWith('fetchSessionStatus'); - }); - }); - - describe('stopPollingSessionStatus', () => { - it('does nothing if sessionStatusInterval is empty', () => { - return testAction(actions.stopPollingSessionStatus, null, {}, [], []); - }); - - it('clears interval', () => { - return testAction( - actions.stopPollingSessionStatus, - null, - { sessionStatusInterval: 7 }, - [{ type: mutationTypes.SET_SESSION_STATUS_INTERVAL, payload: 0 }], - [], - ); - }); - }); - - describe('receiveSessionStatusSuccess', () => { - it('sets session status', () => { - return testAction( - actions.receiveSessionStatusSuccess, - { status: RUNNING }, - {}, - [{ type: mutationTypes.SET_SESSION_STATUS, payload: RUNNING }], - [], - ); - }); - - [STOPPING, STOPPED, 'unexpected'].forEach((status) => { - it(`kills session if status is ${status}`, () => { - return testAction( - actions.receiveSessionStatusSuccess, - { status }, - {}, - [{ type: mutationTypes.SET_SESSION_STATUS, payload: status }], - [{ type: 'killSession' }], - ); - }); - }); - }); - - describe('receiveSessionStatusError', () => { - it('shows an alert', () => { - actions.receiveSessionStatusError({ dispatch }); - - expect(createAlert).toHaveBeenCalledWith({ - message: messages.UNEXPECTED_ERROR_STATUS, - }); - }); - - it('kills the session', () => { - return testAction(actions.receiveSessionStatusError, null, {}, [], [{ type: 'killSession' }]); - }); - }); - - describe('fetchSessionStatus', () => { - let state; - - beforeEach(() => { - state = { - session: { - showPath: TEST_SESSION.show_path, - }, - }; - }); - - it('does nothing if session is falsey', () => { - state.session = null; - - actions.fetchSessionStatus({ dispatch, state }); - - expect(dispatch).not.toHaveBeenCalled(); - }); - - it('dispatches success on success', () => { - mock.onGet(state.session.showPath).reply(HTTP_STATUS_OK, TEST_SESSION); - - return testAction( - actions.fetchSessionStatus, - null, - state, - [], - [{ type: 'receiveSessionStatusSuccess', payload: TEST_SESSION }], - ); - }); - - it('dispatches error on error', () => { - mock.onGet(state.session.showPath).reply(HTTP_STATUS_BAD_REQUEST); - - return testAction( - actions.fetchSessionStatus, - null, - state, - [], - [{ type: 'receiveSessionStatusError', payload: expect.any(Error) }], - ); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js deleted file mode 100644 index a823c05c459..00000000000 --- a/spec/frontend/ide/stores/modules/terminal/actions/setup_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import testAction from 'helpers/vuex_action_helper'; -import * as actions from '~/ide/stores/modules/terminal/actions/setup'; -import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types'; - -describe('IDE store terminal setup actions', () => { - describe('init', () => { - it('dispatches checks', () => { - return testAction( - actions.init, - null, - {}, - [], - [{ type: 'fetchConfigCheck' }, { type: 'fetchRunnersCheck' }], - ); - }); - }); - - describe('hideSplash', () => { - it('commits HIDE_SPLASH', () => { - return testAction(actions.hideSplash, null, {}, [{ type: mutationTypes.HIDE_SPLASH }], []); - }); - }); - - describe('setPaths', () => { - it('commits SET_PATHS', () => { - const paths = { - foo: 'bar', - lorem: 'ipsum', - }; - - return testAction( - actions.setPaths, - paths, - {}, - [{ type: mutationTypes.SET_PATHS, payload: paths }], - [], - ); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/terminal/getters_spec.js b/spec/frontend/ide/stores/modules/terminal/getters_spec.js deleted file mode 100644 index b5d6a4bc746..00000000000 --- a/spec/frontend/ide/stores/modules/terminal/getters_spec.js +++ /dev/null @@ -1,50 +0,0 @@ -import { CHECK_CONFIG, CHECK_RUNNERS } from '~/ide/stores/modules/terminal/constants'; -import * as getters from '~/ide/stores/modules/terminal/getters'; - -describe('IDE store terminal getters', () => { - describe('allCheck', () => { - it('is loading if one check is loading', () => { - const checks = { - [CHECK_CONFIG]: { isLoading: false, isValid: true }, - [CHECK_RUNNERS]: { isLoading: true }, - }; - - const result = getters.allCheck({ checks }); - - expect(result).toEqual({ - isLoading: true, - }); - }); - - it('is invalid if one check is invalid', () => { - const message = 'lorem ipsum'; - const checks = { - [CHECK_CONFIG]: { isLoading: false, isValid: false, message }, - [CHECK_RUNNERS]: { isLoading: false, isValid: true }, - }; - - const result = getters.allCheck({ checks }); - - expect(result).toEqual({ - isLoading: false, - isValid: false, - message, - }); - }); - - it('is valid if all checks are valid', () => { - const checks = { - [CHECK_CONFIG]: { isLoading: false, isValid: true }, - [CHECK_RUNNERS]: { isLoading: false, isValid: true }, - }; - - const result = getters.allCheck({ checks }); - - expect(result).toEqual({ - isLoading: false, - isValid: true, - message: '', - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/terminal/messages_spec.js b/spec/frontend/ide/stores/modules/terminal/messages_spec.js deleted file mode 100644 index f99496a4b98..00000000000 --- a/spec/frontend/ide/stores/modules/terminal/messages_spec.js +++ /dev/null @@ -1,44 +0,0 @@ -import { escape } from 'lodash'; -import { TEST_HOST } from 'spec/test_constants'; -import * as messages from '~/ide/stores/modules/terminal/messages'; -import { - HTTP_STATUS_FORBIDDEN, - HTTP_STATUS_NOT_FOUND, - HTTP_STATUS_UNPROCESSABLE_ENTITY, -} from '~/lib/utils/http_status'; -import { sprintf } from '~/locale'; - -const TEST_HELP_URL = `${TEST_HOST}/help`; - -describe('IDE store terminal messages', () => { - describe('configCheckError', () => { - it('returns job error, with status UNPROCESSABLE_ENTITY', () => { - const result = messages.configCheckError(HTTP_STATUS_UNPROCESSABLE_ENTITY, TEST_HELP_URL); - - expect(result).toBe( - sprintf( - messages.ERROR_CONFIG, - { - codeStart: ``, - codeEnd: ``, - helpStart: ``, - helpEnd: '', - }, - false, - ), - ); - }); - - it('returns permission error, with status FORBIDDEN', () => { - const result = messages.configCheckError(HTTP_STATUS_FORBIDDEN, TEST_HELP_URL); - - expect(result).toBe(messages.ERROR_PERMISSION); - }); - - it('returns unexpected error, with unexpected status', () => { - const result = messages.configCheckError(HTTP_STATUS_NOT_FOUND, TEST_HELP_URL); - - expect(result).toBe(messages.UNEXPECTED_ERROR_CONFIG); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/terminal/mutations_spec.js b/spec/frontend/ide/stores/modules/terminal/mutations_spec.js deleted file mode 100644 index 3451932a185..00000000000 --- a/spec/frontend/ide/stores/modules/terminal/mutations_spec.js +++ /dev/null @@ -1,142 +0,0 @@ -import { - CHECK_CONFIG, - CHECK_RUNNERS, - RUNNING, - STOPPING, -} from '~/ide/stores/modules/terminal/constants'; -import * as types from '~/ide/stores/modules/terminal/mutation_types'; -import mutations from '~/ide/stores/modules/terminal/mutations'; -import createState from '~/ide/stores/modules/terminal/state'; - -describe('IDE store terminal mutations', () => { - let state; - - beforeEach(() => { - state = createState(); - }); - - describe(types.SET_VISIBLE, () => { - it('sets isVisible', () => { - state.isVisible = false; - - mutations[types.SET_VISIBLE](state, true); - - expect(state.isVisible).toBe(true); - }); - }); - - describe(types.HIDE_SPLASH, () => { - it('sets isShowSplash', () => { - state.isShowSplash = true; - - mutations[types.HIDE_SPLASH](state); - - expect(state.isShowSplash).toBe(false); - }); - }); - - describe(types.SET_PATHS, () => { - it('sets paths', () => { - const paths = { - test: 'foo', - }; - - mutations[types.SET_PATHS](state, paths); - - expect(state.paths).toBe(paths); - }); - }); - - describe(types.REQUEST_CHECK, () => { - it('sets isLoading for check', () => { - const type = CHECK_CONFIG; - - state.checks[type] = {}; - mutations[types.REQUEST_CHECK](state, type); - - expect(state.checks[type]).toEqual({ - isLoading: true, - }); - }); - }); - - describe(types.RECEIVE_CHECK_ERROR, () => { - it('sets error for check', () => { - const type = CHECK_RUNNERS; - const message = 'lorem ipsum'; - - state.checks[type] = {}; - mutations[types.RECEIVE_CHECK_ERROR](state, { type, message }); - - expect(state.checks[type]).toEqual({ - isLoading: false, - isValid: false, - message, - }); - }); - }); - - describe(types.RECEIVE_CHECK_SUCCESS, () => { - it('sets success for check', () => { - const type = CHECK_CONFIG; - - state.checks[type] = {}; - mutations[types.RECEIVE_CHECK_SUCCESS](state, type); - - expect(state.checks[type]).toEqual({ - isLoading: false, - isValid: true, - message: null, - }); - }); - }); - - describe(types.SET_SESSION, () => { - it('sets session', () => { - const session = { - terminalPath: 'terminal/foo', - status: RUNNING, - }; - - mutations[types.SET_SESSION](state, session); - - expect(state.session).toBe(session); - }); - }); - - describe(types.SET_SESSION_STATUS, () => { - it('sets session if a session does not exists', () => { - const status = RUNNING; - - mutations[types.SET_SESSION_STATUS](state, status); - - expect(state.session).toEqual({ - status, - }); - }); - - it('sets session status', () => { - state.session = { - terminalPath: 'terminal/foo', - status: RUNNING, - }; - - mutations[types.SET_SESSION_STATUS](state, STOPPING); - - expect(state.session).toEqual({ - terminalPath: 'terminal/foo', - status: STOPPING, - }); - }); - }); - - describe(types.SET_SESSION_STATUS_INTERVAL, () => { - it('sets sessionStatusInterval', () => { - const val = 7; - - mutations[types.SET_SESSION_STATUS_INTERVAL](state, val); - - expect(state.sessionStatusInterval).toEqual(val); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js b/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js deleted file mode 100644 index 448fd909f39..00000000000 --- a/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js +++ /dev/null @@ -1,109 +0,0 @@ -import testAction from 'helpers/vuex_action_helper'; -import mirror, { canConnect, SERVICE_NAME } from '~/ide/lib/mirror'; -import * as actions from '~/ide/stores/modules/terminal_sync/actions'; -import * as types from '~/ide/stores/modules/terminal_sync/mutation_types'; - -jest.mock('~/ide/lib/mirror'); - -const TEST_SESSION = { - proxyWebsocketPath: 'test/path', - services: [SERVICE_NAME], -}; - -describe('ide/stores/modules/terminal_sync/actions', () => { - let rootState; - - beforeEach(() => { - canConnect.mockReturnValue(true); - rootState = { - changedFiles: [], - terminal: {}, - }; - }); - - describe('upload', () => { - it('uploads to mirror and sets success', async () => { - mirror.upload.mockReturnValue(Promise.resolve()); - - await testAction( - actions.upload, - null, - rootState, - [{ type: types.START_LOADING }, { type: types.SET_SUCCESS }], - [], - ); - expect(mirror.upload).toHaveBeenCalledWith(rootState); - }); - - it('sets error when failed', () => { - const err = { message: 'it failed!' }; - mirror.upload.mockReturnValue(Promise.reject(err)); - - return testAction( - actions.upload, - null, - rootState, - [{ type: types.START_LOADING }, { type: types.SET_ERROR, payload: err }], - [], - ); - }); - }); - - describe('stop', () => { - it('disconnects from mirror', async () => { - await testAction(actions.stop, null, rootState, [{ type: types.STOP }], []); - expect(mirror.disconnect).toHaveBeenCalled(); - }); - }); - - describe('start', () => { - it.each` - session | canConnectMock | description - ${null} | ${true} | ${'does not exist'} - ${{}} | ${true} | ${'does not have proxyWebsocketPath'} - ${{ proxyWebsocketPath: 'test/path' }} | ${false} | ${'can not connect service'} - `('rejects if session $description', ({ session, canConnectMock }) => { - canConnect.mockReturnValue(canConnectMock); - - const result = actions.start({ rootState: { terminal: { session } } }); - - return expect(result).rejects.toBe(undefined); - }); - - describe('with terminal session in state', () => { - beforeEach(() => { - rootState = { - terminal: { session: TEST_SESSION }, - }; - }); - - it('connects to mirror and sets success', async () => { - mirror.connect.mockReturnValue(Promise.resolve()); - - await testAction( - actions.start, - null, - rootState, - [{ type: types.START_LOADING }, { type: types.SET_SUCCESS }], - [], - ); - expect(mirror.connect).toHaveBeenCalledWith(TEST_SESSION.proxyWebsocketPath); - }); - - it('sets error if connection fails', () => { - const commit = jest.fn(); - const err = new Error('test'); - mirror.connect.mockReturnValue(Promise.reject(err)); - - const result = actions.start({ rootState, commit }); - - return Promise.all([ - expect(result).rejects.toEqual(err), - result.catch(() => { - expect(commit).toHaveBeenCalledWith(types.SET_ERROR, err); - }), - ]); - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js b/spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js deleted file mode 100644 index b7dbf93f4e6..00000000000 --- a/spec/frontend/ide/stores/modules/terminal_sync/mutations_spec.js +++ /dev/null @@ -1,89 +0,0 @@ -import * as types from '~/ide/stores/modules/terminal_sync/mutation_types'; -import mutations from '~/ide/stores/modules/terminal_sync/mutations'; -import createState from '~/ide/stores/modules/terminal_sync/state'; - -const TEST_MESSAGE = 'lorem ipsum dolar sit'; - -describe('ide/stores/modules/terminal_sync/mutations', () => { - let state; - - beforeEach(() => { - state = createState(); - }); - - describe(types.START_LOADING, () => { - it('sets isLoading and resets error', () => { - Object.assign(state, { - isLoading: false, - isError: true, - }); - - mutations[types.START_LOADING](state); - - expect(state).toEqual( - expect.objectContaining({ - isLoading: true, - isError: false, - }), - ); - }); - }); - - describe(types.SET_ERROR, () => { - it('sets isLoading and error message', () => { - Object.assign(state, { - isLoading: true, - isError: false, - message: '', - }); - - mutations[types.SET_ERROR](state, { message: TEST_MESSAGE }); - - expect(state).toEqual( - expect.objectContaining({ - isLoading: false, - isError: true, - message: TEST_MESSAGE, - }), - ); - }); - }); - - describe(types.SET_SUCCESS, () => { - it('sets isLoading and resets error and is started', () => { - Object.assign(state, { - isLoading: true, - isError: true, - isStarted: false, - }); - - mutations[types.SET_SUCCESS](state); - - expect(state).toEqual( - expect.objectContaining({ - isLoading: false, - isError: false, - isStarted: true, - }), - ); - }); - }); - - describe(types.STOP, () => { - it('sets stop values', () => { - Object.assign(state, { - isLoading: true, - isStarted: true, - }); - - mutations[types.STOP](state); - - expect(state).toEqual( - expect.objectContaining({ - isLoading: false, - isStarted: false, - }), - ); - }); - }); -}); diff --git a/spec/frontend/ide/stores/mutations/branch_spec.js b/spec/frontend/ide/stores/mutations/branch_spec.js deleted file mode 100644 index 30a688d2bb0..00000000000 --- a/spec/frontend/ide/stores/mutations/branch_spec.js +++ /dev/null @@ -1,75 +0,0 @@ -import mutations from '~/ide/stores/mutations/branch'; -import state from '~/ide/stores/state'; - -describe('Multi-file store branch mutations', () => { - let localState; - - beforeEach(() => { - localState = state(); - }); - - describe('SET_CURRENT_BRANCH', () => { - it('sets currentBranch', () => { - mutations.SET_CURRENT_BRANCH(localState, 'main'); - - expect(localState.currentBranchId).toBe('main'); - }); - }); - - describe('SET_BRANCH_COMMIT', () => { - it('sets the last commit on current project', () => { - localState.projects = { - Example: { - branches: { - main: {}, - }, - }, - }; - - mutations.SET_BRANCH_COMMIT(localState, { - projectId: 'Example', - branchId: 'main', - commit: { - title: 'Example commit', - }, - }); - - expect(localState.projects.Example.branches.main.commit.title).toBe('Example commit'); - }); - }); - - describe('SET_BRANCH_WORKING_REFERENCE', () => { - beforeEach(() => { - localState.projects = { - Foo: { - branches: { - bar: {}, - }, - }, - }; - }); - - it('sets workingReference for existing branch', () => { - mutations.SET_BRANCH_WORKING_REFERENCE(localState, { - projectId: 'Foo', - branchId: 'bar', - reference: 'foo-bar-ref', - }); - - expect(localState.projects.Foo.branches.bar.workingReference).toBe('foo-bar-ref'); - }); - - it('does not fail on non-existent just yet branch', () => { - expect(localState.projects.Foo.branches.unknown).toBeUndefined(); - - mutations.SET_BRANCH_WORKING_REFERENCE(localState, { - projectId: 'Foo', - branchId: 'unknown', - reference: 'fun-fun-ref', - }); - - expect(localState.projects.Foo.branches.unknown).not.toBeUndefined(); - expect(localState.projects.Foo.branches.unknown.workingReference).toBe('fun-fun-ref'); - }); - }); -}); diff --git a/spec/frontend/ide/stores/mutations/file_spec.js b/spec/frontend/ide/stores/mutations/file_spec.js deleted file mode 100644 index 69ec2e7a6f5..00000000000 --- a/spec/frontend/ide/stores/mutations/file_spec.js +++ /dev/null @@ -1,581 +0,0 @@ -import { createStore } from '~/ide/stores'; -import mutations from '~/ide/stores/mutations/file'; -import { file } from '../../helpers'; - -describe('IDE store file mutations', () => { - let localState; - let localStore; - let localFile; - - beforeEach(() => { - localStore = createStore(); - localState = localStore.state; - localFile = { ...file('file'), type: 'blob', content: 'original' }; - - localState.entries[localFile.path] = localFile; - }); - - describe('SET_FILE_ACTIVE', () => { - it('sets the file active', () => { - mutations.SET_FILE_ACTIVE(localState, { - path: localFile.path, - active: true, - }); - - expect(localFile.active).toBe(true); - }); - - it('sets pending tab as not active', () => { - localState.openFiles.push({ ...localFile, pending: true, active: true }); - - mutations.SET_FILE_ACTIVE(localState, { - path: localFile.path, - active: true, - }); - - expect(localState.openFiles[0].active).toBe(false); - }); - }); - - describe('TOGGLE_FILE_OPEN', () => { - it('adds into opened files', () => { - mutations.TOGGLE_FILE_OPEN(localState, localFile.path); - - expect(localFile.opened).toBe(true); - expect(localState.openFiles.length).toBe(1); - }); - - describe('if already open', () => { - it('removes from opened files', () => { - mutations.TOGGLE_FILE_OPEN(localState, localFile.path); - mutations.TOGGLE_FILE_OPEN(localState, localFile.path); - - expect(localFile.opened).toBe(false); - expect(localState.openFiles.length).toBe(0); - }); - }); - - it.each` - entry | loading - ${{ opened: false }} | ${true} - ${{ opened: false, tempFile: true }} | ${false} - ${{ opened: true }} | ${false} - `('for state: $entry, sets loading=$loading', ({ entry, loading }) => { - Object.assign(localFile, entry); - - mutations.TOGGLE_FILE_OPEN(localState, localFile.path); - - expect(localFile.loading).toBe(loading); - }); - }); - - describe('SET_FILE_DATA', () => { - it('sets extra file data', () => { - mutations.SET_FILE_DATA(localState, { - data: { - raw_path: 'raw', - }, - file: localFile, - }); - - expect(localFile.rawPath).toBe('raw'); - expect(localFile.raw).toBeNull(); - expect(localFile.baseRaw).toBeNull(); - }); - - it('sets extra file data to all arrays concerned', () => { - localState.stagedFiles = [localFile]; - localState.changedFiles = [localFile]; - localState.openFiles = [localFile]; - - const rawPath = 'foo/bar/blah.md'; - - mutations.SET_FILE_DATA(localState, { - data: { - raw_path: rawPath, - }, - file: localFile, - }); - - expect(localState.stagedFiles[0].rawPath).toEqual(rawPath); - expect(localState.changedFiles[0].rawPath).toEqual(rawPath); - expect(localState.openFiles[0].rawPath).toEqual(rawPath); - expect(localFile.rawPath).toEqual(rawPath); - }); - - it('does not mutate certain props on the file', () => { - const path = 'New Path'; - const name = 'New Name'; - localFile.path = path; - localFile.name = name; - - localState.stagedFiles = [localFile]; - localState.changedFiles = [localFile]; - localState.openFiles = [localFile]; - - mutations.SET_FILE_DATA(localState, { - data: { - path: 'Old Path', - name: 'Old Name', - raw: 'Old Raw', - base_raw: 'Old Base Raw', - }, - file: localFile, - }); - - [ - localState.stagedFiles[0], - localState.changedFiles[0], - localState.openFiles[0], - localFile, - ].forEach((f) => { - expect(f).toEqual( - expect.objectContaining({ - path, - name, - raw: null, - baseRaw: null, - }), - ); - }); - }); - }); - - describe('SET_FILE_RAW_DATA', () => { - const callMutationForFile = (f) => { - mutations.SET_FILE_RAW_DATA(localState, { - file: f, - raw: 'testing', - fileDeletedAndReadded: localStore.getters.isFileDeletedAndReadded(localFile.path), - }); - }; - - it('sets raw data', () => { - callMutationForFile(localFile); - - expect(localFile.raw).toBe('testing'); - }); - - it('sets raw data to stagedFile if file was deleted and readded', () => { - localState.stagedFiles = [{ ...localFile, deleted: true }]; - localFile.tempFile = true; - - callMutationForFile(localFile); - - expect(localFile.raw).toEqual(''); - expect(localState.stagedFiles[0].raw).toBe('testing'); - }); - - it("sets raw data to a file's content if tempFile is empty", () => { - localFile.tempFile = true; - localFile.content = ''; - - callMutationForFile(localFile); - - expect(localFile.raw).toEqual(''); - expect(localFile.content).toBe('testing'); - }); - - it('adds raw data to open pending file', () => { - localState.openFiles.push({ ...localFile, pending: true }); - - callMutationForFile(localFile); - - expect(localState.openFiles[0].raw).toBe('testing'); - }); - - it('sets raw to content of a renamed tempFile', () => { - localFile.tempFile = true; - localFile.prevPath = 'old_path'; - localState.openFiles.push({ ...localFile, pending: true }); - - callMutationForFile(localFile); - - expect(localState.openFiles[0].raw).not.toBe('testing'); - expect(localState.openFiles[0].content).toBe('testing'); - }); - - it('adds raw data to a staged deleted file if unstaged change has a tempFile of the same name', () => { - localFile.tempFile = true; - localState.openFiles.push({ ...localFile, pending: true }); - localState.stagedFiles = [{ ...localFile, deleted: true }]; - - callMutationForFile(localFile); - - expect(localFile.raw).toEqual(''); - expect(localState.stagedFiles[0].raw).toBe('testing'); - }); - }); - - describe('SET_FILE_BASE_RAW_DATA', () => { - it('sets raw data from base branch', () => { - mutations.SET_FILE_BASE_RAW_DATA(localState, { - file: localFile, - baseRaw: 'testing', - }); - - expect(localFile.baseRaw).toBe('testing'); - }); - }); - - describe('UPDATE_FILE_CONTENT', () => { - beforeEach(() => { - localFile.raw = 'test'; - }); - - it('sets content', () => { - mutations.UPDATE_FILE_CONTENT(localState, { - path: localFile.path, - content: 'test', - }); - - expect(localFile.content).toBe('test'); - }); - - it('sets changed if content does not match raw', () => { - mutations.UPDATE_FILE_CONTENT(localState, { - path: localFile.path, - content: 'testing', - }); - - expect(localFile.content).toBe('testing'); - expect(localFile.changed).toBe(true); - }); - - it('sets changed if file is a temp file', () => { - localFile.tempFile = true; - - mutations.UPDATE_FILE_CONTENT(localState, { - path: localFile.path, - content: '', - }); - - expect(localFile.changed).toBe(true); - }); - }); - - describe('SET_FILE_MERGE_REQUEST_CHANGE', () => { - it('sets file mr change', () => { - mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { - file: localFile, - mrChange: { - diff: 'ABC', - }, - }); - - expect(localFile.mrChange.diff).toBe('ABC'); - }); - - it('has diffMode replaced by default', () => { - mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { - file: localFile, - mrChange: { - diff: 'ABC', - }, - }); - - expect(localFile.mrChange.diffMode).toBe('replaced'); - }); - - it('has diffMode new', () => { - mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { - file: localFile, - mrChange: { - diff: 'ABC', - new_file: true, - }, - }); - - expect(localFile.mrChange.diffMode).toBe('new'); - }); - - it('has diffMode deleted', () => { - mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { - file: localFile, - mrChange: { - diff: 'ABC', - deleted_file: true, - }, - }); - - expect(localFile.mrChange.diffMode).toBe('deleted'); - }); - - it('has diffMode renamed', () => { - mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, { - file: localFile, - mrChange: { - diff: 'ABC', - renamed_file: true, - }, - }); - - expect(localFile.mrChange.diffMode).toBe('renamed'); - }); - }); - - describe('DISCARD_FILE_CHANGES', () => { - beforeEach(() => { - localFile.content = 'test'; - localFile.changed = true; - localState.currentProjectId = 'gitlab-ce'; - localState.currentBranchId = 'main'; - localState.trees['gitlab-ce/main'] = { - tree: [], - }; - }); - - it('resets content and changed', () => { - mutations.DISCARD_FILE_CHANGES(localState, localFile.path); - - expect(localFile.content).toBe(''); - expect(localFile.changed).toBe(false); - }); - - it('adds to root tree if deleted', () => { - localFile.deleted = true; - - mutations.DISCARD_FILE_CHANGES(localState, localFile.path); - - expect(localState.trees['gitlab-ce/main'].tree).toEqual([{ ...localFile, deleted: false }]); - }); - - it('adds to parent tree if deleted', () => { - localFile.deleted = true; - localFile.parentPath = 'parentPath'; - localState.entries.parentPath = { - tree: [], - }; - - mutations.DISCARD_FILE_CHANGES(localState, localFile.path); - - expect(localState.entries.parentPath.tree).toEqual([{ ...localFile, deleted: false }]); - }); - }); - - describe('ADD_FILE_TO_CHANGED', () => { - it('adds file into changed files array', () => { - mutations.ADD_FILE_TO_CHANGED(localState, localFile.path); - - expect(localState.changedFiles.length).toBe(1); - }); - }); - - describe('REMOVE_FILE_FROM_CHANGED', () => { - it('removes files from changed files array', () => { - localState.changedFiles.push(localFile); - - mutations.REMOVE_FILE_FROM_CHANGED(localState, localFile.path); - - expect(localState.changedFiles.length).toBe(0); - }); - }); - - describe.each` - mutationName | mutation | addedTo | removedFrom | staged | changedFilesCount | stagedFilesCount - ${'STAGE_CHANGE'} | ${mutations.STAGE_CHANGE} | ${'stagedFiles'} | ${'changedFiles'} | ${true} | ${0} | ${1} - ${'UNSTAGE_CHANGE'} | ${mutations.UNSTAGE_CHANGE} | ${'changedFiles'} | ${'stagedFiles'} | ${false} | ${1} | ${0} - `( - '$mutationName', - ({ mutation, changedFilesCount, removedFrom, addedTo, staged, stagedFilesCount }) => { - let unstagedFile; - let stagedFile; - - beforeEach(() => { - unstagedFile = { - ...file('file'), - type: 'blob', - raw: 'original content', - content: 'changed content', - }; - - stagedFile = { - ...unstagedFile, - content: 'staged content', - staged: true, - }; - - localState.changedFiles.push(unstagedFile); - localState.stagedFiles.push(stagedFile); - localState.entries[unstagedFile.path] = unstagedFile; - }); - - it('removes all changes of a file if staged and unstaged change contents are equal', () => { - unstagedFile.content = 'original content'; - - mutation(localState, { - path: unstagedFile.path, - diffInfo: localStore.getters.getDiffInfo(unstagedFile.path), - }); - - expect(localState.entries.file).toEqual( - expect.objectContaining({ - content: 'original content', - staged: false, - changed: false, - }), - ); - - expect(localState.stagedFiles.length).toBe(0); - expect(localState.changedFiles.length).toBe(0); - }); - - it('removes all changes of a file if a file is deleted and a new file with same content is added', () => { - stagedFile.deleted = true; - unstagedFile.tempFile = true; - unstagedFile.content = 'original content'; - - mutation(localState, { - path: unstagedFile.path, - diffInfo: localStore.getters.getDiffInfo(unstagedFile.path), - }); - - expect(localState.stagedFiles.length).toBe(0); - expect(localState.changedFiles.length).toBe(0); - - expect(localState.entries.file).toEqual( - expect.objectContaining({ - content: 'original content', - deleted: false, - tempFile: false, - }), - ); - }); - - it('merges deleted and added file into a changed file if the contents differ', () => { - stagedFile.deleted = true; - unstagedFile.tempFile = true; - unstagedFile.content = 'hello'; - - mutation(localState, { - path: unstagedFile.path, - diffInfo: localStore.getters.getDiffInfo(unstagedFile.path), - }); - - expect(localState.stagedFiles.length).toBe(stagedFilesCount); - expect(localState.changedFiles.length).toBe(changedFilesCount); - - expect(unstagedFile).toEqual( - expect.objectContaining({ - content: 'hello', - staged, - deleted: false, - tempFile: false, - changed: true, - }), - ); - }); - - it('does not remove file from stagedFiles and changedFiles if the file was renamed, even if the contents are equal', () => { - unstagedFile.content = 'original content'; - unstagedFile.prevPath = 'old_file'; - - mutation(localState, { - path: unstagedFile.path, - diffInfo: localStore.getters.getDiffInfo(unstagedFile.path), - }); - - expect(localState.entries.file).toEqual( - expect.objectContaining({ - content: 'original content', - staged, - changed: false, - prevPath: 'old_file', - }), - ); - - expect(localState.stagedFiles.length).toBe(stagedFilesCount); - expect(localState.changedFiles.length).toBe(changedFilesCount); - }); - - it(`removes file from ${removedFrom} array and adds it into ${addedTo} array`, () => { - localState.stagedFiles.length = 0; - - mutation(localState, { - path: unstagedFile.path, - diffInfo: localStore.getters.getDiffInfo(unstagedFile.path), - }); - - expect(localState.stagedFiles.length).toBe(stagedFilesCount); - expect(localState.changedFiles.length).toBe(changedFilesCount); - - const f = localState.stagedFiles[0] || localState.changedFiles[0]; - expect(f).toEqual(unstagedFile); - }); - - it(`updates file in ${addedTo} array if it is was already present in it`, () => { - unstagedFile.raw = 'testing 123'; - - mutation(localState, { - path: unstagedFile.path, - diffInfo: localStore.getters.getDiffInfo(unstagedFile.path), - }); - - expect(localState.stagedFiles.length).toBe(stagedFilesCount); - expect(localState.changedFiles.length).toBe(changedFilesCount); - - const f = localState.stagedFiles[0] || localState.changedFiles[0]; - expect(f.raw).toEqual('testing 123'); - }); - }, - ); - - describe('TOGGLE_FILE_CHANGED', () => { - it('updates file changed status', () => { - mutations.TOGGLE_FILE_CHANGED(localState, { - file: localFile, - changed: true, - }); - - expect(localFile.changed).toBe(true); - }); - }); - - describe('ADD_PENDING_TAB', () => { - beforeEach(() => { - const f = { ...file('openFile'), path: 'openFile', active: true, opened: true }; - - localState.entries[f.path] = f; - localState.openFiles.push(f); - }); - - it('adds file into openFiles as pending', () => { - mutations.ADD_PENDING_TAB(localState, { - file: localFile, - }); - - expect(localState.openFiles.length).toBe(1); - expect(localState.openFiles[0].pending).toBe(true); - expect(localState.openFiles[0].key).toBe(`pending-${localFile.key}`); - }); - - it('only allows 1 open pending file', () => { - const newFile = file('test'); - localState.entries[newFile.path] = newFile; - - mutations.ADD_PENDING_TAB(localState, { - file: localFile, - }); - - expect(localState.openFiles.length).toBe(1); - - mutations.ADD_PENDING_TAB(localState, { - file: file('test'), - }); - - expect(localState.openFiles.length).toBe(1); - expect(localState.openFiles[0].name).toBe('test'); - }); - }); - - describe('REMOVE_PENDING_TAB', () => { - it('removes pending tab from openFiles', () => { - localFile.key = 'testing'; - localState.openFiles.push(localFile); - - mutations.REMOVE_PENDING_TAB(localState, localFile); - - expect(localState.openFiles.length).toBe(0); - }); - }); -}); diff --git a/spec/frontend/ide/stores/mutations/merge_request_spec.js b/spec/frontend/ide/stores/mutations/merge_request_spec.js deleted file mode 100644 index 2af06835181..00000000000 --- a/spec/frontend/ide/stores/mutations/merge_request_spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import mutations from '~/ide/stores/mutations/merge_request'; -import state from '~/ide/stores/state'; - -describe('IDE store merge request mutations', () => { - let localState; - - beforeEach(() => { - localState = state(); - localState.projects = { abcproject: { mergeRequests: {} } }; - - mutations.SET_MERGE_REQUEST(localState, { - projectPath: 'abcproject', - mergeRequestId: 1, - mergeRequest: { - title: 'mr', - }, - }); - }); - - describe('SET_CURRENT_MERGE_REQUEST', () => { - it('sets current merge request', () => { - mutations.SET_CURRENT_MERGE_REQUEST(localState, 2); - - expect(localState.currentMergeRequestId).toBe(2); - }); - }); - - describe('SET_MERGE_REQUEST', () => { - it('setsmerge request data', () => { - const newMr = localState.projects.abcproject.mergeRequests[1]; - - expect(newMr.title).toBe('mr'); - expect(newMr.active).toBe(true); - }); - - it('keeps original data', () => { - const versions = ['change']; - const mergeRequest = localState.projects.abcproject.mergeRequests[1]; - - mergeRequest.versions = versions; - - mutations.SET_MERGE_REQUEST(localState, { - projectPath: 'abcproject', - mergeRequestId: 1, - mergeRequest: { - title: ['change'], - }, - }); - - expect(mergeRequest.title).toBe('mr'); - expect(mergeRequest.versions).toEqual(versions); - }); - }); - - describe('SET_MERGE_REQUEST_CHANGES', () => { - it('sets merge request changes', () => { - mutations.SET_MERGE_REQUEST_CHANGES(localState, { - projectPath: 'abcproject', - mergeRequestId: 1, - changes: { - diff: 'abc', - }, - }); - - const newMr = localState.projects.abcproject.mergeRequests[1]; - - expect(newMr.changes.diff).toBe('abc'); - }); - }); - - describe('SET_MERGE_REQUEST_VERSIONS', () => { - it('sets merge request versions', () => { - mutations.SET_MERGE_REQUEST_VERSIONS(localState, { - projectPath: 'abcproject', - mergeRequestId: 1, - versions: [{ id: 123 }], - }); - - const newMr = localState.projects.abcproject.mergeRequests[1]; - - expect(newMr.versions.length).toBe(1); - expect(newMr.versions[0].id).toBe(123); - }); - }); -}); diff --git a/spec/frontend/ide/stores/mutations/project_spec.js b/spec/frontend/ide/stores/mutations/project_spec.js deleted file mode 100644 index 0fdd7798f00..00000000000 --- a/spec/frontend/ide/stores/mutations/project_spec.js +++ /dev/null @@ -1,50 +0,0 @@ -import mutations from '~/ide/stores/mutations/project'; -import state from '~/ide/stores/state'; - -describe('Multi-file store branch mutations', () => { - let localState; - const nonExistentProj = 'nonexistent'; - const existingProj = 'abcproject'; - - beforeEach(() => { - localState = state(); - localState.projects = { [existingProj]: { empty_repo: true } }; - }); - - describe('TOGGLE_EMPTY_STATE', () => { - it('sets empty_repo for project to passed value', () => { - mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: existingProj, value: false }); - - expect(localState.projects[existingProj].empty_repo).toBe(false); - - mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: existingProj, value: true }); - - expect(localState.projects[existingProj].empty_repo).toBe(true); - }); - }); - - describe('UPDATE_PROJECT', () => { - it.each` - desc | projectPath | props | expectedProps - ${'extends existing project with the passed props'} | ${existingProj} | ${{ foo1: 'bar' }} | ${{ foo1: 'bar' }} - ${'overrides existing props on the exsiting project'} | ${existingProj} | ${{ empty_repo: false }} | ${{ empty_repo: false }} - ${'does nothing if the project does not exist'} | ${nonExistentProj} | ${{ foo2: 'bar' }} | ${undefined} - ${'does nothing if project is not passed'} | ${undefined} | ${{ foo3: 'bar' }} | ${undefined} - ${'does nothing if the props are not passed'} | ${existingProj} | ${undefined} | ${{}} - ${'does nothing if the props are empty'} | ${existingProj} | ${{}} | ${{}} - `('$desc', ({ projectPath, props, expectedProps } = {}) => { - const origProject = localState.projects[projectPath]; - - mutations.UPDATE_PROJECT(localState, { projectPath, props }); - - if (!expectedProps) { - expect(localState.projects[projectPath]).toBeUndefined(); - } else { - expect(localState.projects[projectPath]).toEqual({ - ...origProject, - ...expectedProps, - }); - } - }); - }); -}); diff --git a/spec/frontend/ide/stores/mutations/tree_spec.js b/spec/frontend/ide/stores/mutations/tree_spec.js deleted file mode 100644 index a8c0d7ba2c8..00000000000 --- a/spec/frontend/ide/stores/mutations/tree_spec.js +++ /dev/null @@ -1,118 +0,0 @@ -import mutations from '~/ide/stores/mutations/tree'; -import state from '~/ide/stores/state'; -import { file } from '../../helpers'; - -describe('Multi-file store tree mutations', () => { - let localState; - let localTree; - - beforeEach(() => { - localState = state(); - localTree = file(); - - localState.entries[localTree.path] = localTree; - }); - - describe('TOGGLE_TREE_OPEN', () => { - it('toggles tree open', () => { - mutations.TOGGLE_TREE_OPEN(localState, localTree.path); - - expect(localTree.opened).toBe(true); - - mutations.TOGGLE_TREE_OPEN(localState, localTree.path); - - expect(localTree.opened).toBe(false); - }); - }); - - describe('SET_DIRECTORY_DATA', () => { - let data; - - beforeEach(() => { - data = [file('tree'), file('foo'), file('blob')]; - }); - - it('adds directory data', () => { - localState.trees['project/main'] = { - tree: [], - }; - - mutations.SET_DIRECTORY_DATA(localState, { - data, - treePath: 'project/main', - }); - - const tree = localState.trees['project/main']; - - expect(tree.tree.length).toBe(3); - expect(tree.tree[0].name).toBe('tree'); - expect(tree.tree[1].name).toBe('foo'); - expect(tree.tree[2].name).toBe('blob'); - }); - - it('keeps loading state', () => { - mutations.CREATE_TREE(localState, { - treePath: 'project/main', - }); - mutations.SET_DIRECTORY_DATA(localState, { - data, - treePath: 'project/main', - }); - - expect(localState.trees['project/main'].loading).toBe(true); - }); - - it('does not override tree already in state, but merges the two with correct order', () => { - const openedFile = file('new'); - - localState.trees['project/main'] = { - loading: true, - tree: [openedFile], - }; - - mutations.SET_DIRECTORY_DATA(localState, { - data, - treePath: 'project/main', - }); - - const { tree } = localState.trees['project/main']; - - expect(tree.length).toBe(4); - expect(tree[0].name).toBe('blob'); - expect(tree[1].name).toBe('foo'); - expect(tree[2].name).toBe('new'); - expect(tree[3].name).toBe('tree'); - }); - - it('returns tree unchanged if the opened file is already in the tree', () => { - const openedFile = file('foo'); - localState.trees['project/main'] = { - loading: true, - tree: [openedFile], - }; - - mutations.SET_DIRECTORY_DATA(localState, { - data, - treePath: 'project/main', - }); - - const { tree } = localState.trees['project/main']; - - expect(tree.length).toBe(3); - - expect(tree[0].name).toBe('tree'); - expect(tree[1].name).toBe('foo'); - expect(tree[2].name).toBe('blob'); - }); - }); - - describe('REMOVE_ALL_CHANGES_FILES', () => { - it('removes all files from changedFiles state', () => { - localState.changedFiles.push(file('REMOVE_ALL_CHANGES_FILES')); - - mutations.REMOVE_ALL_CHANGES_FILES(localState); - - expect(localState.changedFiles.length).toBe(0); - }); - }); -}); diff --git a/spec/frontend/ide/stores/mutations_spec.js b/spec/frontend/ide/stores/mutations_spec.js deleted file mode 100644 index ae21d257bb2..00000000000 --- a/spec/frontend/ide/stores/mutations_spec.js +++ /dev/null @@ -1,660 +0,0 @@ -import { TEST_HOST } from 'helpers/test_constants'; -import mutations from '~/ide/stores/mutations'; -import state from '~/ide/stores/state'; -import { file } from '../helpers'; - -describe('Multi-file store mutations', () => { - let localState; - let entry; - - beforeEach(() => { - localState = state(); - entry = file(); - - localState.entries[entry.path] = entry; - }); - - describe('SET_INITIAL_DATA', () => { - it('sets all initial data', () => { - mutations.SET_INITIAL_DATA(localState, { - test: 'test', - }); - - expect(localState.test).toBe('test'); - }); - }); - - describe('TOGGLE_LOADING', () => { - it('toggles loading of entry', () => { - mutations.TOGGLE_LOADING(localState, { - entry, - }); - - expect(entry.loading).toBe(true); - - mutations.TOGGLE_LOADING(localState, { - entry, - }); - - expect(entry.loading).toBe(false); - }); - - it('toggles loading of entry and sets specific value', () => { - mutations.TOGGLE_LOADING(localState, { - entry, - }); - - expect(entry.loading).toBe(true); - - mutations.TOGGLE_LOADING(localState, { - entry, - forceValue: true, - }); - - expect(entry.loading).toBe(true); - }); - }); - - describe('CLEAR_STAGED_CHANGES', () => { - it('clears stagedFiles array', () => { - localState.stagedFiles.push('a'); - - mutations.CLEAR_STAGED_CHANGES(localState); - - expect(localState.stagedFiles.length).toBe(0); - }); - }); - - describe('UPDATE_VIEWER', () => { - it('sets viewer state', () => { - mutations.UPDATE_VIEWER(localState, 'diff'); - - expect(localState.viewer).toBe('diff'); - }); - }); - - describe('UPDATE_ACTIVITY_BAR_VIEW', () => { - it('updates currentActivityBar', () => { - mutations.UPDATE_ACTIVITY_BAR_VIEW(localState, 'test'); - - expect(localState.currentActivityView).toBe('test'); - }); - }); - - describe('SET_EMPTY_STATE_SVGS', () => { - it('updates empty state SVGs', () => { - mutations.SET_EMPTY_STATE_SVGS(localState, { - emptyStateSvgPath: 'emptyState', - noChangesStateSvgPath: 'noChanges', - committedStateSvgPath: 'committed', - switchEditorSvgPath: 'switchEditorSvg', - }); - - expect(localState.emptyStateSvgPath).toBe('emptyState'); - expect(localState.noChangesStateSvgPath).toBe('noChanges'); - expect(localState.committedStateSvgPath).toBe('committed'); - expect(localState.switchEditorSvgPath).toBe('switchEditorSvg'); - }); - }); - - describe('CREATE_TMP_ENTRY', () => { - beforeEach(() => { - localState.currentProjectId = 'gitlab-ce'; - localState.currentBranchId = 'main'; - localState.trees['gitlab-ce/main'] = { - tree: [], - }; - }); - - it('creates temp entry in the tree', () => { - const tmpFile = file('test'); - mutations.CREATE_TMP_ENTRY(localState, { - data: { - entries: { - test: { ...tmpFile, tempFile: true, changed: true }, - }, - treeList: [tmpFile], - }, - }); - - expect(localState.trees['gitlab-ce/main'].tree.length).toEqual(1); - expect(localState.entries.test.tempFile).toEqual(true); - }); - }); - - describe('UPDATE_TEMP_FLAG', () => { - beforeEach(() => { - localState.entries.test = { ...file(), tempFile: true, changed: true }; - }); - - it('updates tempFile flag', () => { - mutations.UPDATE_TEMP_FLAG(localState, { - path: 'test', - tempFile: false, - }); - - expect(localState.entries.test.tempFile).toBe(false); - }); - - it('updates changed flag', () => { - mutations.UPDATE_TEMP_FLAG(localState, { - path: 'test', - tempFile: false, - }); - - expect(localState.entries.test.changed).toBe(false); - }); - }); - - describe('TOGGLE_FILE_FINDER', () => { - it('updates fileFindVisible', () => { - mutations.TOGGLE_FILE_FINDER(localState, true); - - expect(localState.fileFindVisible).toBe(true); - }); - }); - - describe('SET_ERROR_MESSAGE', () => { - it('updates error message', () => { - mutations.SET_ERROR_MESSAGE(localState, 'error'); - - expect(localState.errorMessage).toBe('error'); - }); - }); - - describe('DELETE_ENTRY', () => { - beforeEach(() => { - localState.currentProjectId = 'gitlab-ce'; - localState.currentBranchId = 'main'; - localState.trees['gitlab-ce/main'] = { - tree: [], - }; - }); - - it('sets deleted flag', () => { - localState.entries.filePath = { - deleted: false, - }; - - mutations.DELETE_ENTRY(localState, 'filePath'); - - expect(localState.entries.filePath.deleted).toBe(true); - }); - - it('removes from root tree', () => { - localState.entries.filePath = { - path: 'filePath', - deleted: false, - }; - localState.trees['gitlab-ce/main'].tree.push(localState.entries.filePath); - - mutations.DELETE_ENTRY(localState, 'filePath'); - - expect(localState.trees['gitlab-ce/main'].tree).toEqual([]); - }); - - it('removes from parent tree', () => { - localState.entries.filePath = { - path: 'filePath', - deleted: false, - parentPath: 'parentPath', - }; - localState.entries.parentPath = { - tree: [localState.entries.filePath], - }; - - mutations.DELETE_ENTRY(localState, 'filePath'); - - expect(localState.entries.parentPath.tree).toEqual([]); - }); - - it('adds to changedFiles', () => { - localState.entries.filePath = { - deleted: false, - type: 'blob', - }; - - mutations.DELETE_ENTRY(localState, 'filePath'); - - expect(localState.changedFiles).toEqual([localState.entries.filePath]); - }); - - it('does not add tempFile into changedFiles', () => { - localState.entries.filePath = { - deleted: false, - type: 'blob', - tempFile: true, - }; - - mutations.DELETE_ENTRY(localState, 'filePath'); - - expect(localState.changedFiles).toEqual([]); - }); - - it('removes tempFile from changedFiles and stagedFiles when deleted', () => { - localState.entries.filePath = { - path: 'filePath', - deleted: false, - type: 'blob', - tempFile: true, - }; - - localState.changedFiles.push({ ...localState.entries.filePath }); - localState.stagedFiles.push({ ...localState.entries.filePath }); - - mutations.DELETE_ENTRY(localState, 'filePath'); - - expect(localState.changedFiles).toEqual([]); - expect(localState.stagedFiles).toEqual([]); - }); - }); - - describe('UPDATE_FILE_AFTER_COMMIT', () => { - it('updates URLs if prevPath is set', () => { - const f = { - ...file('test'), - prevPath: 'testing-123', - rawPath: `${TEST_HOST}/testing-123`, - }; - localState.entries.test = f; - localState.changedFiles.push(f); - - mutations.UPDATE_FILE_AFTER_COMMIT(localState, { - file: f, - lastCommit: { - commit: {}, - }, - }); - - expect(f).toEqual( - expect.objectContaining({ - rawPath: `${TEST_HOST}/test`, - prevId: undefined, - prevPath: undefined, - prevName: undefined, - prevKey: undefined, - }), - ); - }); - }); - - describe('RENAME_ENTRY', () => { - beforeEach(() => { - localState.trees = { - 'gitlab-ce/main': { - tree: [], - }, - }; - localState.currentProjectId = 'gitlab-ce'; - localState.currentBranchId = 'main'; - localState.entries = { - oldPath: file('oldPath', 'oldPath', 'blob'), - }; - }); - - it('updates existing entry without creating a new one', () => { - mutations.RENAME_ENTRY(localState, { - path: 'oldPath', - name: 'newPath', - parentPath: '', - }); - - expect(localState.entries).toEqual({ - newPath: expect.objectContaining({ - path: 'newPath', - prevPath: 'oldPath', - }), - }); - }); - - it('correctly handles consecutive renames for the same entry', () => { - mutations.RENAME_ENTRY(localState, { - path: 'oldPath', - name: 'newPath', - parentPath: '', - }); - - mutations.RENAME_ENTRY(localState, { - path: 'newPath', - name: 'newestPath', - parentPath: '', - }); - - expect(localState.entries).toEqual({ - newestPath: expect.objectContaining({ - path: 'newestPath', - prevPath: 'oldPath', - }), - }); - }); - - it('correctly handles the same entry within a consecutively renamed folder', () => { - const oldPath = file('root-folder/oldPath', 'root-folder/oldPath', 'blob'); - localState.entries = { - 'root-folder': { ...file('root-folder', 'root-folder', 'tree'), tree: [oldPath] }, - 'root-folder/oldPath': oldPath, - }; - Object.assign(localState.entries['root-folder/oldPath'], { - parentPath: 'root-folder', - }); - - mutations.RENAME_ENTRY(localState, { - path: 'root-folder/oldPath', - name: 'renamed-folder/oldPath', - entryPath: null, - parentPath: '', - }); - - mutations.RENAME_ENTRY(localState, { - path: 'renamed-folder/oldPath', - name: 'simply-renamed/oldPath', - entryPath: null, - parentPath: '', - }); - - expect(localState.entries).toEqual({ - 'root-folder': expect.objectContaining({ - path: 'root-folder', - }), - 'simply-renamed/oldPath': expect.objectContaining({ - path: 'simply-renamed/oldPath', - prevPath: 'root-folder/oldPath', - }), - }); - }); - - it('renames entry, preserving old parameters', () => { - const oldPathData = localState.entries.oldPath; - - mutations.RENAME_ENTRY(localState, { - path: 'oldPath', - name: 'newPath', - parentPath: '', - }); - - expect(localState.entries.newPath).toEqual({ - ...oldPathData, - id: 'newPath', - path: 'newPath', - name: 'newPath', - key: expect.stringMatching('newPath'), - prevId: 'oldPath', - prevName: 'oldPath', - prevPath: 'oldPath', - prevKey: oldPathData.key, - prevParentPath: oldPathData.parentPath, - }); - }); - - it('does not store previous attributes on temp files', () => { - Object.assign(localState.entries.oldPath, { - tempFile: true, - }); - mutations.RENAME_ENTRY(localState, { - path: 'oldPath', - name: 'newPath', - entryPath: null, - parentPath: '', - }); - - expect(localState.entries.newPath).not.toEqual( - expect.objectContaining({ - prevId: expect.anything(), - prevName: expect.anything(), - prevPath: expect.anything(), - prevKey: expect.anything(), - prevParentPath: expect.anything(), - }), - ); - }); - - it('properly handles files with spaces in name', () => { - const path = 'my fancy path'; - const newPath = 'new path'; - const oldEntry = file(path, path, 'blob'); - - localState.entries[path] = oldEntry; - - mutations.RENAME_ENTRY(localState, { - path, - name: newPath, - entryPath: null, - parentPath: '', - }); - - expect(localState.entries[newPath]).toEqual({ - ...oldEntry, - id: newPath, - path: newPath, - name: newPath, - key: expect.stringMatching(newPath), - prevId: path, - prevName: path, - prevPath: path, - prevKey: oldEntry.key, - prevParentPath: oldEntry.parentPath, - }); - }); - - it('adds to parent tree', () => { - const parentEntry = { - ...file('parentPath', 'parentPath', 'tree'), - tree: [localState.entries.oldPath], - }; - localState.entries.parentPath = parentEntry; - - mutations.RENAME_ENTRY(localState, { - path: 'oldPath', - name: 'newPath', - entryPath: null, - parentPath: 'parentPath', - }); - - expect(parentEntry.tree.length).toBe(1); - expect(parentEntry.tree[0].name).toBe('newPath'); - }); - - it('sorts tree after renaming an entry', () => { - const alpha = file('alpha', 'alpha', 'blob'); - const beta = file('beta', 'beta', 'blob'); - const gamma = file('gamma', 'gamma', 'blob'); - localState.entries = { - alpha, - beta, - gamma, - }; - - localState.trees['gitlab-ce/main'].tree = [alpha, beta, gamma]; - - mutations.RENAME_ENTRY(localState, { - path: 'alpha', - name: 'theta', - entryPath: null, - parentPath: '', - }); - - expect(localState.trees['gitlab-ce/main'].tree).toEqual([ - expect.objectContaining({ - name: 'beta', - }), - expect.objectContaining({ - name: 'gamma', - }), - expect.objectContaining({ - path: 'theta', - name: 'theta', - }), - ]); - }); - - it('updates openFiles with the renamed one if the original one is open', () => { - Object.assign(localState.entries.oldPath, { - opened: true, - type: 'blob', - }); - Object.assign(localState, { - openFiles: [localState.entries.oldPath], - }); - - mutations.RENAME_ENTRY(localState, { - path: 'oldPath', - name: 'newPath', - }); - - expect(localState.openFiles.length).toBe(1); - expect(localState.openFiles[0].path).toBe('newPath'); - }); - - it('does not add renamed entry to changedFiles', () => { - mutations.RENAME_ENTRY(localState, { - path: 'oldPath', - name: 'newPath', - }); - - expect(localState.changedFiles.length).toBe(0); - }); - - it('updates existing changedFiles entry with the renamed one', () => { - const origFile = { ...file('oldPath', 'oldPath', 'blob'), content: 'Foo' }; - - Object.assign(localState, { - changedFiles: [origFile], - }); - Object.assign(localState.entries, { - oldPath: origFile, - }); - - mutations.RENAME_ENTRY(localState, { - path: 'oldPath', - name: 'newPath', - }); - - expect(localState.changedFiles).toEqual([ - expect.objectContaining({ - path: 'newPath', - content: 'Foo', - }), - ]); - }); - - it('correctly saves original values if an entry is renamed multiple times', () => { - const original = { ...localState.entries.oldPath }; - const paramsToCheck = ['prevId', 'prevPath', 'prevName']; - const expectedObj = paramsToCheck.reduce( - (o, param) => ({ ...o, [param]: original[param.replace('prev', '').toLowerCase()] }), - {}, - ); - - mutations.RENAME_ENTRY(localState, { - path: 'oldPath', - name: 'newPath', - }); - - expect(localState.entries.newPath).toEqual(expect.objectContaining(expectedObj)); - - mutations.RENAME_ENTRY(localState, { - path: 'newPath', - name: 'newer', - }); - - expect(localState.entries.newer).toEqual(expect.objectContaining(expectedObj)); - }); - - describe('renaming back to original', () => { - beforeEach(() => { - const renamedEntry = { - ...file('renamed', 'renamed', 'blob'), - prevId: 'lorem/orig', - prevPath: 'lorem/orig', - prevName: 'orig', - prevKey: 'lorem/orig', - prevParentPath: 'lorem', - }; - - localState.entries = { - renamed: renamedEntry, - }; - - mutations.RENAME_ENTRY(localState, { - path: 'renamed', - name: 'orig', - parentPath: 'lorem', - }); - }); - - it('renames entry and clears prev properties', () => { - expect(localState.entries).toEqual({ - 'lorem/orig': expect.objectContaining({ - id: 'lorem/orig', - path: 'lorem/orig', - name: 'orig', - prevId: undefined, - prevPath: undefined, - prevName: undefined, - prevKey: undefined, - prevParentPath: undefined, - }), - }); - }); - }); - - describe('key updates', () => { - beforeEach(() => { - const rootFolder = file('rootFolder', 'rootFolder', 'tree'); - localState.entries = { - rootFolder, - oldPath: file('oldPath', 'oldPath', 'blob'), - 'oldPath.txt': file('oldPath.txt', 'oldPath.txt', 'blob'), - 'rootFolder/oldPath.md': file('oldPath.md', 'oldPath.md', 'blob', rootFolder), - }; - }); - - it('sets properly constucted key while preserving the original one', () => { - const key = 'oldPath.txt-blob-oldPath.txt'; - localState.entries['oldPath.txt'].key = key; - mutations.RENAME_ENTRY(localState, { - path: 'oldPath.txt', - name: 'newPath.md', - }); - - expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md'); - expect(localState.entries['newPath.md'].prevKey).toBe(key); - }); - - it('correctly updates key for an entry without an extension', () => { - localState.entries.oldPath.key = 'oldPath-blob-oldPath'; - mutations.RENAME_ENTRY(localState, { - path: 'oldPath', - name: 'newPath.md', - }); - - expect(localState.entries['newPath.md'].key).toBe('newPath.md-blob-newPath.md'); - }); - - it('correctly updates key when new name does not have an extension', () => { - localState.entries['oldPath.txt'].key = 'oldPath.txt-blob-oldPath.txt'; - mutations.RENAME_ENTRY(localState, { - path: 'oldPath.txt', - name: 'newPath', - }); - - expect(localState.entries.newPath.key).toBe('newPath-blob-newPath'); - }); - - it('correctly updates key when renaming an entry in a folder', () => { - localState.entries['rootFolder/oldPath.md'].key = - 'rootFolder/oldPath.md-blob-rootFolder/oldPath.md'; - mutations.RENAME_ENTRY(localState, { - path: 'rootFolder/oldPath.md', - name: 'newPath.md', - entryPath: null, - parentPath: 'rootFolder', - }); - - expect(localState.entries['rootFolder/newPath.md'].key).toBe( - 'rootFolder/newPath.md-blob-rootFolder/newPath.md', - ); - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/plugins/terminal_spec.js b/spec/frontend/ide/stores/plugins/terminal_spec.js deleted file mode 100644 index b2d5d85e005..00000000000 --- a/spec/frontend/ide/stores/plugins/terminal_spec.js +++ /dev/null @@ -1,58 +0,0 @@ -import Vue from 'vue'; -// eslint-disable-next-line no-restricted-imports -import Vuex from 'vuex'; -import { TEST_HOST } from 'helpers/test_constants'; -import terminalModule from '~/ide/stores/modules/terminal'; -import { SET_BRANCH_WORKING_REFERENCE } from '~/ide/stores/mutation_types'; -import createTerminalPlugin from '~/ide/stores/plugins/terminal'; - -const TEST_DATASET = { - webTerminalSvgPath: `${TEST_HOST}/web/terminal/svg`, - webTerminalHelpPath: `${TEST_HOST}/web/terminal/help`, - webTerminalConfigHelpPath: `${TEST_HOST}/web/terminal/config/help`, - webTerminalRunnersHelpPath: `${TEST_HOST}/web/terminal/runners/help`, -}; -Vue.use(Vuex); - -describe('ide/stores/extend', () => { - let store; - - beforeEach(() => { - const el = document.createElement('div'); - Object.assign(el.dataset, TEST_DATASET); - - store = new Vuex.Store({ - mutations: { - [SET_BRANCH_WORKING_REFERENCE]: () => {}, - }, - }); - - jest.spyOn(store, 'registerModule').mockImplementation(); - jest.spyOn(store, 'dispatch').mockImplementation(); - - const plugin = createTerminalPlugin(el); - - plugin(store); - }); - - it('registers terminal module', () => { - expect(store.registerModule).toHaveBeenCalledWith('terminal', terminalModule()); - }); - - it('dispatches terminal/setPaths', () => { - expect(store.dispatch).toHaveBeenCalledWith('terminal/setPaths', { - webTerminalSvgPath: TEST_DATASET.webTerminalSvgPath, - webTerminalHelpPath: TEST_DATASET.webTerminalHelpPath, - webTerminalConfigHelpPath: TEST_DATASET.webTerminalConfigHelpPath, - webTerminalRunnersHelpPath: TEST_DATASET.webTerminalRunnersHelpPath, - }); - }); - - it(`dispatches terminal/init on ${SET_BRANCH_WORKING_REFERENCE}`, () => { - store.dispatch.mockReset(); - - store.commit(SET_BRANCH_WORKING_REFERENCE); - - expect(store.dispatch).toHaveBeenCalledWith('terminal/init'); - }); -}); diff --git a/spec/frontend/ide/stores/plugins/terminal_sync_spec.js b/spec/frontend/ide/stores/plugins/terminal_sync_spec.js deleted file mode 100644 index f12f80c1602..00000000000 --- a/spec/frontend/ide/stores/plugins/terminal_sync_spec.js +++ /dev/null @@ -1,81 +0,0 @@ -import eventHub from '~/ide/eventhub'; -import { createStore } from '~/ide/stores'; -import { RUNNING, STOPPING } from '~/ide/stores/modules/terminal/constants'; -import { SET_SESSION_STATUS } from '~/ide/stores/modules/terminal/mutation_types'; -import createTerminalPlugin from '~/ide/stores/plugins/terminal'; -import createTerminalSyncPlugin from '~/ide/stores/plugins/terminal_sync'; -import { createTriggerUpdatePayload } from '../../helpers'; - -jest.mock('~/ide/lib/mirror'); - -const ACTION_START = 'terminalSync/start'; -const ACTION_STOP = 'terminalSync/stop'; -const ACTION_UPLOAD = 'terminalSync/upload'; -const FILES_CHANGE_EVENT = 'ide.files.change'; - -describe('IDE stores/plugins/mirror', () => { - let store; - - beforeEach(() => { - const root = document.createElement('div'); - - store = createStore(); - createTerminalPlugin(root)(store); - - store.dispatch = jest.fn(() => Promise.resolve()); - - createTerminalSyncPlugin(root)(store); - }); - - it('does nothing on ide.files.change event', () => { - eventHub.$emit(FILES_CHANGE_EVENT); - - expect(store.dispatch).not.toHaveBeenCalled(); - }); - - describe('when session starts running', () => { - beforeEach(() => { - store.commit(`terminal/${SET_SESSION_STATUS}`, RUNNING); - }); - - it('starts', () => { - expect(store.dispatch).toHaveBeenCalledWith(ACTION_START); - }); - - it('uploads when ide.files.change is emitted', () => { - expect(store.dispatch).not.toHaveBeenCalledWith(ACTION_UPLOAD); - - eventHub.$emit(FILES_CHANGE_EVENT); - - jest.runAllTimers(); - - expect(store.dispatch).toHaveBeenCalledWith(ACTION_UPLOAD); - }); - - it('does nothing when ide.files.change is emitted with "update"', () => { - eventHub.$emit(FILES_CHANGE_EVENT, createTriggerUpdatePayload('foo')); - - jest.runAllTimers(); - - expect(store.dispatch).not.toHaveBeenCalledWith(ACTION_UPLOAD); - }); - - describe('when session stops', () => { - beforeEach(() => { - store.commit(`terminal/${SET_SESSION_STATUS}`, STOPPING); - }); - - it('stops', () => { - expect(store.dispatch).toHaveBeenCalledWith(ACTION_STOP); - }); - - it('does not upload anymore', () => { - eventHub.$emit(FILES_CHANGE_EVENT); - - jest.runAllTimers(); - - expect(store.dispatch).not.toHaveBeenCalledWith(ACTION_UPLOAD); - }); - }); - }); -}); diff --git a/spec/frontend/ide/stores/utils_spec.js b/spec/frontend/ide/stores/utils_spec.js deleted file mode 100644 index a8875e0cd02..00000000000 --- a/spec/frontend/ide/stores/utils_spec.js +++ /dev/null @@ -1,672 +0,0 @@ -import { commitActionTypes } from '~/ide/constants'; -import * as utils from '~/ide/stores/utils'; -import { file } from '../helpers'; - -describe('Multi-file store utils', () => { - describe('setPageTitle', () => { - it('sets the document page title', () => { - utils.setPageTitle('test'); - - expect(document.title).toBe('test'); - }); - }); - - describe('setPageTitleForFile', () => { - it('sets the document page title for the file passed', () => { - const f = { - path: 'README.md', - }; - - const state = { - currentBranchId: 'main', - currentProjectId: 'test/test', - }; - - utils.setPageTitleForFile(state, f); - - expect(document.title).toBe('README.md · main · test/test · GitLab'); - }); - }); - - describe('createCommitPayload', () => { - it('returns API payload', () => { - const state = { - commitMessage: 'commit message', - }; - const rootState = { - stagedFiles: [ - { - ...file('staged'), - path: 'staged', - content: 'updated file content', - lastCommitSha: '123456789', - }, - { - ...file('newFile'), - path: 'added', - tempFile: true, - content: 'new file content', - rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b', - lastCommitSha: '123456789', - }, - { ...file('deletedFile'), path: 'deletedFile', deleted: true }, - { ...file('renamedFile'), path: 'renamedFile', prevPath: 'prevPath' }, - ], - currentBranchId: 'main', - }; - const payload = utils.createCommitPayload({ - branch: 'main', - newBranch: false, - state, - rootState, - getters: {}, - }); - - expect(payload).toEqual({ - branch: 'main', - commit_message: 'commit message', - actions: [ - { - action: commitActionTypes.update, - file_path: 'staged', - content: 'updated file content', - encoding: 'text', - last_commit_id: '123456789', - previous_path: undefined, - }, - { - action: commitActionTypes.create, - file_path: 'added', - // atob("new file content") - content: 'bmV3IGZpbGUgY29udGVudA==', - encoding: 'base64', - last_commit_id: '123456789', - previous_path: undefined, - }, - { - action: commitActionTypes.delete, - file_path: 'deletedFile', - content: undefined, - encoding: 'text', - last_commit_id: undefined, - previous_path: undefined, - }, - { - action: commitActionTypes.move, - file_path: 'renamedFile', - content: undefined, - encoding: 'text', - last_commit_id: undefined, - previous_path: 'prevPath', - }, - ], - start_sha: undefined, - }); - }); - - it('uses prebuilt commit message when commit message is empty', () => { - const rootState = { - stagedFiles: [ - { - ...file('staged'), - path: 'staged', - content: 'updated file content', - lastCommitSha: '123456789', - }, - { - ...file('newFile'), - path: 'added', - tempFile: true, - content: 'new file content', - rawPath: 'blob:https://gitlab.com/048c7ac1-98de-4a37-ab1b-0206d0ea7e1b', - lastCommitSha: '123456789', - }, - ], - currentBranchId: 'main', - }; - const payload = utils.createCommitPayload({ - branch: 'main', - newBranch: false, - state: {}, - rootState, - getters: { - preBuiltCommitMessage: 'prebuilt test commit message', - }, - }); - - expect(payload).toEqual({ - branch: 'main', - commit_message: 'prebuilt test commit message', - actions: [ - { - action: commitActionTypes.update, - file_path: 'staged', - content: 'updated file content', - encoding: 'text', - last_commit_id: '123456789', - previous_path: undefined, - }, - { - action: commitActionTypes.create, - file_path: 'added', - // atob("new file content") - content: 'bmV3IGZpbGUgY29udGVudA==', - encoding: 'base64', - last_commit_id: '123456789', - previous_path: undefined, - }, - ], - start_sha: undefined, - }); - }); - }); - - describe('commitActionForFile', () => { - it('returns deleted for deleted file', () => { - expect( - utils.commitActionForFile({ - deleted: true, - }), - ).toBe(commitActionTypes.delete); - }); - - it('returns create for tempFile', () => { - expect( - utils.commitActionForFile({ - tempFile: true, - }), - ).toBe(commitActionTypes.create); - }); - - it('returns move for moved file', () => { - expect( - utils.commitActionForFile({ - prevPath: 'test', - }), - ).toBe(commitActionTypes.move); - }); - - it('returns update by default', () => { - expect(utils.commitActionForFile({})).toBe(commitActionTypes.update); - }); - }); - - describe('getCommitFiles', () => { - it('filters out folders from the list', () => { - const files = [ - { - path: 'a', - type: 'blob', - deleted: true, - }, - { - path: 'c', - type: 'tree', - deleted: true, - }, - { - path: 'c/d', - type: 'blob', - deleted: true, - }, - ]; - - const flattendFiles = utils.getCommitFiles(files); - - expect(flattendFiles).toEqual([ - { - path: 'a', - type: 'blob', - deleted: true, - }, - { - path: 'c/d', - type: 'blob', - deleted: true, - }, - ]); - }); - }); - - describe('mergeTrees', () => { - let fromTree; - let toTree; - - beforeEach(() => { - fromTree = [file('foo')]; - toTree = [file('bar')]; - }); - - it('merges simple trees with sorting the result', () => { - toTree = [file('beta'), file('alpha'), file('gamma')]; - const res = utils.mergeTrees(fromTree, toTree); - - expect(res.length).toEqual(4); - expect(res[0].name).toEqual('alpha'); - expect(res[1].name).toEqual('beta'); - expect(res[2].name).toEqual('foo'); - expect(res[3].name).toEqual('gamma'); - expect(res[2]).toBe(fromTree[0]); - }); - - it('handles edge cases', () => { - expect(utils.mergeTrees({}, []).length).toEqual(0); - - let res = utils.mergeTrees({}, toTree); - - expect(res.length).toEqual(1); - expect(res[0].name).toEqual('bar'); - - res = utils.mergeTrees(fromTree, []); - - expect(res.length).toEqual(1); - expect(res[0].name).toEqual('foo'); - expect(res[0]).toBe(fromTree[0]); - }); - - it('merges simple trees without producing duplicates', () => { - toTree.push(file('foo')); - - const res = utils.mergeTrees(fromTree, toTree); - - expect(res.length).toEqual(2); - expect(res[0].name).toEqual('bar'); - expect(res[1].name).toEqual('foo'); - expect(res[1]).not.toBe(fromTree[0]); - }); - - it('merges nested tree into the main one without duplicates', () => { - fromTree[0].tree.push({ - ...file('alpha'), - path: 'foo/alpha', - tree: [{ ...file('beta.md'), path: 'foo/alpha/beta.md' }], - }); - - toTree.push({ - ...file('foo'), - tree: [ - { - ...file('alpha'), - path: 'foo/alpha', - tree: [{ ...file('gamma.md'), path: 'foo/alpha/gamma.md' }], - }, - ], - }); - - const res = utils.mergeTrees(fromTree, toTree); - - expect(res.length).toEqual(2); - expect(res[1].name).toEqual('foo'); - - const finalBranch = res[1].tree[0].tree; - - expect(finalBranch.length).toEqual(2); - expect(finalBranch[0].name).toEqual('beta.md'); - expect(finalBranch[1].name).toEqual('gamma.md'); - }); - - it('marks correct folders as opened as the parsing goes on', () => { - fromTree[0].tree.push({ - ...file('alpha'), - path: 'foo/alpha', - tree: [{ ...file('beta.md'), path: 'foo/alpha/beta.md' }], - }); - - toTree.push({ - ...file('foo'), - tree: [ - { - ...file('alpha'), - path: 'foo/alpha', - tree: [{ ...file('gamma.md'), path: 'foo/alpha/gamma.md' }], - }, - ], - }); - - const res = utils.mergeTrees(fromTree, toTree); - - expect(res[1].name).toEqual('foo'); - expect(res[1].opened).toEqual(true); - - expect(res[1].tree[0].name).toEqual('alpha'); - expect(res[1].tree[0].opened).toEqual(true); - }); - }); - - describe('swapInStateArray', () => { - let localState; - - beforeEach(() => { - localState = []; - }); - - it('swaps existing entry with a new one', () => { - const file1 = { ...file('old'), key: 'foo' }; - const file2 = file('new'); - const arr = [file1]; - - Object.assign(localState, { - dummyArray: arr, - entries: { - new: file2, - }, - }); - - utils.swapInStateArray(localState, 'dummyArray', 'foo', 'new'); - - expect(localState.dummyArray.length).toBe(1); - expect(localState.dummyArray[0]).toBe(file2); - }); - - it('does not add an item if it does not exist yet in array', () => { - const file1 = file('file'); - Object.assign(localState, { - dummyArray: [], - entries: { - file: file1, - }, - }); - - utils.swapInStateArray(localState, 'dummyArray', 'foo', 'file'); - - expect(localState.dummyArray.length).toBe(0); - }); - }); - - describe('swapInParentTreeWithSorting', () => { - let localState; - let branchInfo; - const currentProjectId = '123-foo'; - const currentBranchId = 'main'; - - beforeEach(() => { - localState = { - currentBranchId, - currentProjectId, - trees: { - [`${currentProjectId}/${currentBranchId}`]: { - tree: [], - }, - }, - entries: { - oldPath: file('oldPath', 'oldPath', 'blob'), - newPath: file('newPath', 'newPath', 'blob'), - parentPath: file('parentPath', 'parentPath', 'tree'), - }, - }; - branchInfo = localState.trees[`${currentProjectId}/${currentBranchId}`]; - }); - - it('does not change tree if newPath is not supplied', () => { - branchInfo.tree = [localState.entries.oldPath]; - - utils.swapInParentTreeWithSorting(localState, 'oldPath', undefined, undefined); - - expect(branchInfo.tree).toEqual([localState.entries.oldPath]); - }); - - describe('oldPath to replace is not defined: simple addition to tree', () => { - it('adds to tree on the state if there is no parent for the entry', () => { - expect(branchInfo.tree.length).toBe(0); - - utils.swapInParentTreeWithSorting(localState, undefined, 'oldPath', undefined); - - expect(branchInfo.tree.length).toBe(1); - expect(branchInfo.tree[0].name).toBe('oldPath'); - - utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', undefined); - - expect(branchInfo.tree.length).toBe(2); - expect(branchInfo.tree).toEqual([ - expect.objectContaining({ - name: 'newPath', - }), - expect.objectContaining({ - name: 'oldPath', - }), - ]); - }); - - it('adds to parent tree if it is supplied', () => { - utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath'); - - expect(localState.entries.parentPath.tree.length).toBe(1); - expect(localState.entries.parentPath.tree).toEqual([ - expect.objectContaining({ - name: 'newPath', - }), - ]); - - localState.entries.parentPath.tree = [localState.entries.oldPath]; - - utils.swapInParentTreeWithSorting(localState, undefined, 'newPath', 'parentPath'); - - expect(localState.entries.parentPath.tree.length).toBe(2); - expect(localState.entries.parentPath.tree).toEqual([ - expect.objectContaining({ - name: 'newPath', - }), - expect.objectContaining({ - name: 'oldPath', - }), - ]); - }); - }); - - describe('swapping of the items', () => { - it('swaps entries if both paths are supplied', () => { - branchInfo.tree = [localState.entries.oldPath]; - - utils.swapInParentTreeWithSorting(localState, localState.entries.oldPath.key, 'newPath'); - - expect(branchInfo.tree).toEqual([ - expect.objectContaining({ - name: 'newPath', - }), - ]); - - utils.swapInParentTreeWithSorting(localState, localState.entries.newPath.key, 'oldPath'); - - expect(branchInfo.tree).toEqual([ - expect.objectContaining({ - name: 'oldPath', - }), - ]); - }); - - it('sorts tree after swapping the entries', () => { - const alpha = file('alpha', 'alpha', 'blob'); - const beta = file('beta', 'beta', 'blob'); - const gamma = file('gamma', 'gamma', 'blob'); - const theta = file('theta', 'theta', 'blob'); - localState.entries = { - alpha, - beta, - gamma, - theta, - }; - - branchInfo.tree = [alpha, beta, gamma]; - - utils.swapInParentTreeWithSorting(localState, alpha.key, 'theta'); - - expect(branchInfo.tree).toEqual([ - expect.objectContaining({ - name: 'beta', - }), - expect.objectContaining({ - name: 'gamma', - }), - expect.objectContaining({ - name: 'theta', - }), - ]); - - utils.swapInParentTreeWithSorting(localState, gamma.key, 'alpha'); - - expect(branchInfo.tree).toEqual([ - expect.objectContaining({ - name: 'alpha', - }), - expect.objectContaining({ - name: 'beta', - }), - expect.objectContaining({ - name: 'theta', - }), - ]); - - utils.swapInParentTreeWithSorting(localState, beta.key, 'gamma'); - - expect(branchInfo.tree).toEqual([ - expect.objectContaining({ - name: 'alpha', - }), - expect.objectContaining({ - name: 'gamma', - }), - expect.objectContaining({ - name: 'theta', - }), - ]); - }); - }); - }); - - describe('cleanTrailingSlash', () => { - [ - { - input: '', - output: '', - }, - { - input: 'abc', - output: 'abc', - }, - { - input: 'abc/', - output: 'abc', - }, - { - input: 'abc/def', - output: 'abc/def', - }, - { - input: 'abc/def/', - output: 'abc/def', - }, - ].forEach(({ input, output }) => { - it(`cleans trailing slash from string "${input}"`, () => { - expect(utils.cleanTrailingSlash(input)).toEqual(output); - }); - }); - }); - - describe('pathsAreEqual', () => { - [ - { - args: ['abc', 'abc'], - output: true, - }, - { - args: ['abc', 'def'], - output: false, - }, - { - args: ['abc/', 'abc'], - output: true, - }, - { - args: ['abc/abc', 'abc'], - output: false, - }, - { - args: ['/', ''], - output: true, - }, - { - args: ['', '/'], - output: true, - }, - { - args: [false, '/'], - output: true, - }, - ].forEach(({ args, output }) => { - it(`cleans and tests equality (${JSON.stringify(args)})`, () => { - expect(utils.pathsAreEqual(...args)).toEqual(output); - }); - }); - }); - - describe('extractMarkdownImagesFromEntries', () => { - let mdFile; - let entries; - - beforeEach(() => { - const img = { content: 'png-gibberish', rawPath: 'blob:1234' }; - mdFile = { path: 'path/to/some/directory/myfile.md' }; - entries = { - // invalid (or lack of) extensions are also supported as long as there's - // a real image inside and can go into an tag's `src` and the browser - // can render it - img, - 'img.js': img, - 'img.png': img, - 'img.with.many.dots.png': img, - 'path/to/img.gif': img, - 'path/to/some/img.jpg': img, - 'path/to/some/img 1/img.png': img, - 'path/to/some/directory/img.png': img, - 'path/to/some/directory/img 1.png': img, - }; - }); - - it.each` - markdownBefore | ext | imgAlt | imgTitle - ${'* ![img](/img)'} | ${'jpeg'} | ${'img'} | ${undefined} - ${'* ![img](/img.js)'} | ${'js'} | ${'img'} | ${undefined} - ${'* ![img](img.png)'} | ${'png'} | ${'img'} | ${undefined} - ${'* ![img](./img.png)'} | ${'png'} | ${'img'} | ${undefined} - ${'* ![with spaces](../img 1/img.png)'} | ${'png'} | ${'with spaces'} | ${undefined} - ${'* ![img](../../img.gif " title ")'} | ${'gif'} | ${'img'} | ${' title '} - ${'* ![img](../img.jpg)'} | ${'jpg'} | ${'img'} | ${undefined} - ${'* ![img](/img.png "title")'} | ${'png'} | ${'img'} | ${'title'} - ${'* ![img](/img.with.many.dots.png)'} | ${'png'} | ${'img'} | ${undefined} - ${'* ![img](img 1.png)'} | ${'png'} | ${'img'} | ${undefined} - ${'* ![img](img.png "title here")'} | ${'png'} | ${'img'} | ${'title here'} - `( - 'correctly transforms markdown with uncommitted images: $markdownBefore', - ({ markdownBefore, imgAlt, imgTitle }) => { - mdFile.content = markdownBefore; - - expect(utils.extractMarkdownImagesFromEntries(mdFile, entries)).toEqual({ - content: '* {{gl_md_img_1}}', - images: { - '{{gl_md_img_1}}': { - src: 'blob:1234', - alt: imgAlt, - title: imgTitle, - }, - }, - }); - }, - ); - - it.each` - markdown - ${'* ![img](i.png)'} - ${'* ![img](img.png invalid title)'} - ${'* ![img](img.png "incorrect" "markdown")'} - ${'* ![img](https://gitlab.com/logo.png)'} - ${'* ![img](https://gitlab.com/some/deep/nested/path/logo.png)'} - `("doesn't touch invalid or non-existant images in markdown: $markdown", ({ markdown }) => { - mdFile.content = markdown; - - expect(utils.extractMarkdownImagesFromEntries(mdFile, entries)).toEqual({ - content: markdown, - images: {}, - }); - }); - }); -}); diff --git a/spec/frontend/ide/sync_router_and_store_spec.js b/spec/frontend/ide/sync_router_and_store_spec.js deleted file mode 100644 index 510bd123d63..00000000000 --- a/spec/frontend/ide/sync_router_and_store_spec.js +++ /dev/null @@ -1,160 +0,0 @@ -import VueRouter from 'vue-router'; -import waitForPromises from 'helpers/wait_for_promises'; -import { describeSkipVue3, SkipReason } from 'helpers/vue3_conditional'; -import { createStore } from '~/ide/stores'; -import { syncRouterAndStore } from '~/ide/sync_router_and_store'; - -const TEST_ROUTE = '/test/lorem/ipsum'; - -const skipReason = new SkipReason({ - name: '~/ide/sync_router_and_store', - reason: 'Legacy WebIDE is due for deletion', - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/508949', -}); -describeSkipVue3(skipReason, () => { - let unsync; - let router; - let store; - let onRouterChange; - - const createSync = () => { - unsync = syncRouterAndStore(router, store); - }; - - const getRouterCurrentPath = () => router.currentRoute.fullPath; - const getStoreCurrentPath = () => store.state.router.fullPath; - const updateRouter = async (path) => { - if (getRouterCurrentPath() === path) { - return; - } - - router.push(path); - await waitForPromises(); - }; - const updateStore = (path) => { - store.dispatch('router/push', path); - return waitForPromises(); - }; - - beforeEach(() => { - router = new VueRouter(); - store = createStore(); - jest.spyOn(store, 'dispatch'); - - onRouterChange = jest.fn(); - router.beforeEach((to, from, next) => { - onRouterChange(to, from); - next(); - }); - }); - - afterEach(() => { - unsync(); - unsync = null; - }); - - it('keeps store and router in sync', async () => { - createSync(); - - await updateRouter('/test/test'); - await updateRouter('/test/test'); - await updateStore('123/abc'); - await updateRouter('def'); - - // Even though we pused relative paths, the store and router kept track of the resulting fullPath - expect(getRouterCurrentPath()).toBe('/test/123/def'); - expect(getStoreCurrentPath()).toBe('/test/123/def'); - }); - - describe('default', () => { - beforeEach(() => { - createSync(); - }); - - it('store is default', () => { - expect(store.dispatch).not.toHaveBeenCalled(); - expect(getStoreCurrentPath()).toBe(''); - }); - - it('router is default', () => { - expect(onRouterChange).not.toHaveBeenCalled(); - expect(getRouterCurrentPath()).toBe('/'); - }); - - describe('when store changes', () => { - beforeEach(() => { - updateStore(TEST_ROUTE); - }); - - it('store is updated', () => { - // let's make sure the action isn't dispatched more than necessary - expect(store.dispatch).toHaveBeenCalledTimes(1); - expect(getStoreCurrentPath()).toBe(TEST_ROUTE); - }); - - it('router is updated', () => { - expect(onRouterChange).toHaveBeenCalledTimes(1); - expect(getRouterCurrentPath()).toBe(TEST_ROUTE); - }); - - describe('when store changes again to the same thing', () => { - beforeEach(() => { - onRouterChange.mockClear(); - updateStore(TEST_ROUTE); - }); - - it('doesnt change router again', () => { - expect(onRouterChange).not.toHaveBeenCalled(); - }); - }); - }); - - describe('when router changes', () => { - beforeEach(() => { - updateRouter(TEST_ROUTE); - }); - - it('store is updated', () => { - expect(store.dispatch).toHaveBeenCalledTimes(1); - expect(getStoreCurrentPath()).toBe(TEST_ROUTE); - }); - - it('router is updated', () => { - // let's make sure the router change isn't triggered more than necessary - expect(onRouterChange).toHaveBeenCalledTimes(1); - expect(getRouterCurrentPath()).toBe(TEST_ROUTE); - }); - - describe('when router changes again to the same thing', () => { - beforeEach(() => { - store.dispatch.mockClear(); - updateRouter(TEST_ROUTE); - }); - - it('doesnt change store again', () => { - expect(store.dispatch).not.toHaveBeenCalled(); - }); - }); - }); - - describe('when disposed', () => { - beforeEach(() => { - unsync(); - }); - - it('a store change does not trigger a router change', () => { - updateStore(TEST_ROUTE); - - expect(getRouterCurrentPath()).toBe('/'); - expect(onRouterChange).not.toHaveBeenCalled(); - }); - - it('a router change does not trigger a store change', () => { - updateRouter(TEST_ROUTE); - - expect(getStoreCurrentPath()).toBe(''); - expect(store.dispatch).not.toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js index dd3c6862ea4..7376850cac6 100644 --- a/spec/frontend/ide/utils_spec.js +++ b/spec/frontend/ide/utils_spec.js @@ -1,116 +1,8 @@ import { languages } from 'monaco-editor'; import { setDiagnosticsOptions as yamlDiagnosticsOptions } from 'monaco-yaml'; -import { - isTextFile, - registerLanguages, - registerSchema, - trimPathComponents, - trimTrailingWhitespace, - getPathParents, - getPathParent, - addNumericSuffix, -} from '~/ide/utils'; +import { registerLanguages, registerSchema } from '~/ide/utils'; describe('WebIDE utils', () => { - describe('isTextFile', () => { - it.each` - mimeType | name | type | result - ${'image/png'} | ${'my.png'} | ${'binary'} | ${false} - ${'IMAGE/PNG'} | ${'my.png'} | ${'binary'} | ${false} - ${'text/plain'} | ${'my.txt'} | ${'text'} | ${true} - ${'TEXT/PLAIN'} | ${'my.txt'} | ${'text'} | ${true} - `('returns $result for known $type types', ({ mimeType, name, result }) => { - expect(isTextFile({ content: 'file content', mimeType, name })).toBe(result); - }); - - it.each` - content | mimeType | name - ${'{"éêė":"value"}'} | ${'application/json'} | ${'my.json'} - ${'{"éêė":"value"}'} | ${'application/json'} | ${'.tsconfig'} - ${'SELECT "éêė" from tablename'} | ${'application/sql'} | ${'my.sql'} - ${'{"éêė":"value"}'} | ${'application/json'} | ${'MY.JSON'} - ${'SELECT "éêė" from tablename'} | ${'application/sql'} | ${'MY.SQL'} - ${'var code = "something"'} | ${'application/javascript'} | ${'Gruntfile'} - ${'MAINTAINER Александр "a21283@me.com"'} | ${'application/octet-stream'} | ${'dockerfile'} - `( - 'returns true for file extensions that Monaco supports syntax highlighting for', - ({ content, mimeType, name }) => { - expect(isTextFile({ content, mimeType, name })).toBe(true); - }, - ); - - it('returns false if filename is same as the expected extension', () => { - expect( - isTextFile({ - name: 'sql', - content: 'SELECT "éêė" from tablename', - mimeType: 'application/sql', - }), - ).toBe(false); - }); - - it('returns true for ASCII only content for unknown types', () => { - expect( - isTextFile({ - name: 'hello.mytype', - content: 'plain text', - mimeType: 'application/x-new-type', - }), - ).toBe(true); - }); - - it('returns false for non-ASCII content for unknown types', () => { - expect( - isTextFile({ - name: 'my.random', - content: '{"éêė":"value"}', - mimeType: 'application/octet-stream', - }), - ).toBe(false); - }); - - it.each` - name | result - ${'myfile.txt'} | ${true} - ${'Dockerfile'} | ${true} - ${'img.png'} | ${false} - ${'abc.js'} | ${true} - ${'abc.random'} | ${false} - ${'image.jpeg'} | ${false} - `('returns $result for $filename when no content or mimeType is passed', ({ name, result }) => { - expect(isTextFile({ name })).toBe(result); - }); - - it('returns true if content is empty string but false if content is not passed', () => { - expect(isTextFile({ name: 'abc.dat' })).toBe(false); - expect(isTextFile({ name: 'abc.dat', content: '' })).toBe(true); - expect(isTextFile({ name: 'abc.dat', content: ' ' })).toBe(true); - }); - - it('returns true if there is a `binary` property already set on the file object', () => { - expect(isTextFile({ name: 'abc.txt', content: '' })).toBe(true); - expect(isTextFile({ name: 'abc.txt', content: '', binary: true })).toBe(false); - - expect(isTextFile({ name: 'abc.tex', content: 'éêė' })).toBe(false); - expect(isTextFile({ name: 'abc.tex', content: 'éêė', binary: false })).toBe(true); - }); - }); - - describe('trimPathComponents', () => { - it.each` - input | output - ${'example path '} | ${'example path'} - ${'p/somefile '} | ${'p/somefile'} - ${'p /somefile '} | ${'p/somefile'} - ${'p/ somefile '} | ${'p/somefile'} - ${' p/somefile '} | ${'p/somefile'} - ${'p/somefile .md'} | ${'p/somefile .md'} - ${'path / to / some/file.doc '} | ${'path/to/some/file.doc'} - `('trims all path components in path: "$input"', ({ input, output }) => { - expect(trimPathComponents(input)).toEqual(output); - }); - }); - describe('registerLanguages', () => { let langs; @@ -216,92 +108,4 @@ describe('WebIDE utils', () => { ); }); }); - - describe('trimTrailingWhitespace', () => { - it.each` - input | output - ${'text \n more text \n'} | ${'text\n more text\n'} - ${'text \n more text \n\n \n'} | ${'text\n more text\n\n\n'} - ${'text \t\t \n more text \n\t\ttext\n \n\t\t'} | ${'text\n more text\n\t\ttext\n\n'} - ${'text \r\n more text \r\n'} | ${'text\r\n more text\r\n'} - ${'text \r\n more text \r\n\r\n \r\n'} | ${'text\r\n more text\r\n\r\n\r\n'} - ${'text \t\t \r\n more text \r\n\t\ttext\r\n \r\n\t\t'} | ${'text\r\n more text\r\n\t\ttext\r\n\r\n'} - `("trims trailing whitespace in each line of file's contents: $input", ({ input, output }) => { - expect(trimTrailingWhitespace(input)).toBe(output); - }); - }); - - describe('getPathParents', () => { - it.each` - path | parents - ${'foo/bar/baz/index.md'} | ${['foo/bar/baz', 'foo/bar', 'foo']} - ${'foo/bar/baz'} | ${['foo/bar', 'foo']} - ${'index.md'} | ${[]} - ${'path with/spaces to/something.md'} | ${['path with/spaces to', 'path with']} - `('gets all parent directory names for path: $path', ({ path, parents }) => { - expect(getPathParents(path)).toEqual(parents); - }); - - it.each` - path | depth | parents - ${'foo/bar/baz/index.md'} | ${0} | ${[]} - ${'foo/bar/baz/index.md'} | ${1} | ${['foo/bar/baz']} - ${'foo/bar/baz/index.md'} | ${2} | ${['foo/bar/baz', 'foo/bar']} - ${'foo/bar/baz/index.md'} | ${3} | ${['foo/bar/baz', 'foo/bar', 'foo']} - ${'foo/bar/baz/index.md'} | ${4} | ${['foo/bar/baz', 'foo/bar', 'foo']} - `('gets only the immediate $depth parents if when depth=$depth', ({ path, depth, parents }) => { - expect(getPathParents(path, depth)).toEqual(parents); - }); - }); - - describe('getPathParent', () => { - it.each` - path | parents - ${'foo/bar/baz/index.md'} | ${'foo/bar/baz'} - ${'foo/bar/baz'} | ${'foo/bar'} - ${'index.md'} | ${undefined} - ${'path with/spaces to/something.md'} | ${'path with/spaces to'} - `('gets the immediate parent for path: $path', ({ path, parents }) => { - expect(getPathParent(path)).toEqual(parents); - }); - }); - - /* - * hello-2425 -> hello-2425 - * hello.md -> hello-1.md - * hello_2.md -> hello_3.md - * hello_ -> hello_1 - * main-patch-22432 -> main-patch-22433 - * patch_332 -> patch_333 - */ - - describe('addNumericSuffix', () => { - it.each` - input | output - ${'hello'} | ${'hello-1'} - ${'hello2'} | ${'hello-3'} - ${'hello.md'} | ${'hello-1.md'} - ${'hello_2.md'} | ${'hello_3.md'} - ${'hello_'} | ${'hello_1'} - ${'main-patch-22432'} | ${'main-patch-22433'} - ${'patch_332'} | ${'patch_333'} - `('adds a numeric suffix to a given filename/branch name: $input', ({ input, output }) => { - expect(addNumericSuffix(input)).toBe(output); - }); - - it.each` - input | output - ${'hello'} | ${'hello-39135'} - ${'hello2'} | ${'hello-39135'} - ${'hello.md'} | ${'hello-39135.md'} - ${'hello_2.md'} | ${'hello_39135.md'} - ${'hello_'} | ${'hello_39135'} - ${'main-patch-22432'} | ${'main-patch-39135'} - ${'patch_332'} | ${'patch_39135'} - `('adds a random suffix if randomize=true is passed for name: $input', ({ input, output }) => { - jest.spyOn(Math, 'random').mockReturnValue(0.391352525); - - expect(addNumericSuffix(input, true)).toBe(output); - }); - }); }); diff --git a/spec/frontend_integration/ide/helpers/ide_helper.js b/spec/frontend_integration/ide/helpers/ide_helper.js deleted file mode 100644 index 40ec0198cd2..00000000000 --- a/spec/frontend_integration/ide/helpers/ide_helper.js +++ /dev/null @@ -1,222 +0,0 @@ -import { - findAllByText, - fireEvent, - getByLabelText, - findByTestId, - getByText, - screen, - findByText, -} from '@testing-library/dom'; -import { editor as monacoEditor } from 'monaco-editor'; - -const isFolderRowOpen = (row) => row.matches('.folder.is-open'); - -const getLeftSidebar = () => screen.getByTestId('left-sidebar'); - -export const switchLeftSidebarTab = (name) => { - const sidebar = getLeftSidebar(); - - const button = getByLabelText(sidebar, name); - - button.click(); -}; - -export const getStatusBar = () => document.querySelector('.ide-status-bar'); - -export const waitForMonacoEditor = () => - new Promise((resolve) => { - monacoEditor.onDidCreateEditor(resolve); - }); - -export const waitForEditorDispose = (instance) => - new Promise((resolve) => { - instance.onDidDispose(resolve); - }); - -export const waitForEditorModelChange = (instance) => - new Promise((resolve) => { - instance.onDidChangeModel(resolve); - }); - -export const findMonacoEditor = () => - screen.findAllByLabelText(/^Code Editor\./).then(([x]) => x.closest('.monaco-editor')); - -export const findMonacoDiffEditor = () => - screen.findAllByLabelText(/^Code Editor\./).then(([x]) => x.closest('.monaco-diff-editor')); - -export const findAndSetEditorValue = async (value) => { - const editor = await findMonacoEditor(); - const { uri } = editor.dataset; - - monacoEditor.getModel(uri).setValue(value); -}; - -export const getEditorValue = async () => { - const editor = await findMonacoEditor(); - const { uri } = editor.dataset; - - return monacoEditor.getModel(uri).getValue(); -}; - -const findTreeBody = () => screen.findByTestId('ide-tree-body'); - -const findRootActions = () => screen.findByTestId('ide-root-actions'); - -const findFileRowContainer = (row = null) => - row ? Promise.resolve(row.parentElement) : findTreeBody(); - -const findFileChild = async (row, name, index = 0) => { - const container = await findFileRowContainer(row); - const children = await findAllByText(container, name, { selector: '.file-row-name' }); - - return children - .map((x) => x.closest('.file-row')) - .find((x) => x.dataset.level === index.toString()); -}; - -const openFileRow = (row) => { - if (!row || isFolderRowOpen(row)) { - return; - } - - row.click(); -}; - -export const findAndTraverseToPath = async (path, index = 0, row = null) => { - if (!path) { - return row; - } - - const [name, ...restOfPath] = path.split('/'); - - openFileRow(row); - - const child = await findFileChild(row, name, index); - - return findAndTraverseToPath(restOfPath.join('/'), index + 1, child); -}; - -const clickFileRowAction = (row, name) => { - fireEvent.mouseOver(row); - - const dropdownButton = getByLabelText(row, 'Create new file or directory'); - dropdownButton.click(); - - const dropdownAction = getByLabelText(dropdownButton.parentNode, name); - dropdownAction.click(); -}; - -const fillFileNameModal = async (value, submitText = 'Create file') => { - const modal = await screen.findByTestId('ide-new-entry'); - - const nameField = await findByTestId(modal, 'file-name-field'); - fireEvent.input(nameField, { target: { value } }); - - const createButton = getByText(modal, submitText, { selector: 'button > span' }); - createButton.click(); -}; - -const findAndClickRootAction = async (name) => { - const container = await findRootActions(); - const button = getByLabelText(container, name); - - button.click(); -}; - -/** - * Drop leading "/-/ide" and file path from the current URL - */ -export const getBaseRoute = (url = window.location.pathname) => - url.replace(/^\/-\/ide/, '').replace(/\/-\/.*$/, ''); - -export const clickPreviewMarkdown = () => { - screen.getByText('Preview Markdown').click(); -}; - -export const openFile = async (path) => { - const row = await findAndTraverseToPath(path); - - openFileRow(row); -}; - -export const waitForTabToOpen = (fileName) => - findByText(document.querySelector('.multi-file-edit-pane'), fileName); - -export const createFile = async (path, content) => { - const parentPath = path.split('/').slice(0, -1).join('/'); - - const parentRow = await findAndTraverseToPath(parentPath); - - if (parentRow) { - clickFileRowAction(parentRow, 'New file'); - } else { - await findAndClickRootAction('New file'); - } - - await fillFileNameModal(path); - await findAndSetEditorValue(content); -}; - -export const updateFile = async (path, content) => { - await openFile(path); - await findAndSetEditorValue(content); -}; - -export const getFilesList = () => { - return screen.getAllByTestId('file-row-name-container').map((e) => e.textContent.trim()); -}; - -export const deleteFile = async (path) => { - const row = await findAndTraverseToPath(path); - clickFileRowAction(row, 'Delete'); -}; - -export const renameFile = async (path, newPath) => { - const row = await findAndTraverseToPath(path); - clickFileRowAction(row, 'Rename/Move'); - - await fillFileNameModal(newPath, 'Rename file'); -}; - -export const closeFile = async (path) => { - const button = await screen.getByLabelText(`Close ${path}`, { - selector: '.multi-file-tabs button', - }); - - button.click(); -}; - -/** - * Fill out and submit the commit form in the Web IDE - * - * @param {Object} options - Used to fill out the commit form in the IDE - * @param {Boolean} options.newBranch - Flag for the "Create a new branch" radio. - * @param {Boolean} options.newMR - Flag for the "Start a new merge request" checkbox. - * @param {String} options.newBranchName - Value to put in the new branch name input field. The Web IDE supports leaving this field blank. - */ -export const commit = async ({ newBranch = false, newMR = false, newBranchName = '' } = {}) => { - switchLeftSidebarTab('Commit'); - screen.getByTestId('begin-commit-button').click(); - - await waitForMonacoEditor(); - - const mrCheck = await screen.findByLabelText('Start a new merge request'); - if (Boolean(mrCheck.checked) !== newMR) { - mrCheck.click(); - } - - if (!newBranch) { - const option = await screen.findByLabelText(/Commit to .+ branch/); - await option.click(); - } else { - const option = await screen.findByLabelText('Create a new branch'); - await option.click(); - - const branchNameInput = await screen.findByTestId('ide-new-branch-name'); - fireEvent.input(branchNameInput, { target: { value: newBranchName } }); - } - - screen.getByText('Commit').click(); - - await waitForMonacoEditor(); -}; diff --git a/spec/frontend_integration/ide/helpers/mock_data.js b/spec/frontend_integration/ide/helpers/mock_data.js deleted file mode 100644 index 5a69d8adeb7..00000000000 --- a/spec/frontend_integration/ide/helpers/mock_data.js +++ /dev/null @@ -1,8 +0,0 @@ -export const IDE_DATASET = { - emptyStateSvgPath: '/test/empty_state.svg', - noChangesStateSvgPath: '/test/no_changes_state.svg', - committedStateSvgPath: '/test/committed_state.svg', - pipelinesEmptyStateSvgPath: '/test/pipelines_empty_state.svg', - webIDEHelpPagePath: '/test/web_ide_help_page', - renderWhitespaceInCode: 'false', -}; diff --git a/spec/frontend_integration/ide/helpers/start.js b/spec/frontend_integration/ide/helpers/start.js deleted file mode 100644 index d31dd235869..00000000000 --- a/spec/frontend_integration/ide/helpers/start.js +++ /dev/null @@ -1,25 +0,0 @@ -import { editor as monacoEditor } from 'monaco-editor'; -import setWindowLocation from 'helpers/set_window_location_helper'; -import { TEST_HOST } from 'helpers/test_constants'; -import { initLegacyWebIDE } from '~/ide/init_legacy_web_ide'; -import extendStore from '~/ide/stores/extend'; -import { getProject, getEmptyProject } from 'jest/../frontend_integration/test_helpers/fixtures'; -import { IDE_DATASET } from './mock_data'; - -export default (container, { isRepoEmpty = false, path = '', mrId = '' } = {}) => { - const projectName = isRepoEmpty ? 'lorem-ipsum-empty' : 'lorem-ipsum'; - const pathSuffix = mrId ? `merge_requests/${mrId}` : `tree/master/-/${path}`; - const project = isRepoEmpty ? getEmptyProject() : getProject(); - - setWindowLocation(`${TEST_HOST}/-/ide/project/gitlab-test/${projectName}/${pathSuffix}`); - - const el = document.createElement('div'); - Object.assign(el.dataset, IDE_DATASET, { project: JSON.stringify(project) }); - container.appendChild(el); - 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())); - - return vm; -}; diff --git a/spec/frontend_integration/ide/ide_integration_spec.js b/spec/frontend_integration/ide/ide_integration_spec.js deleted file mode 100644 index f454dd98130..00000000000 --- a/spec/frontend_integration/ide/ide_integration_spec.js +++ /dev/null @@ -1,180 +0,0 @@ -import { nextTick } from 'vue'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import waitForPromises from 'helpers/wait_for_promises'; -import { waitForText } from 'helpers/wait_for_text'; -import { useOverclockTimers } from 'test_helpers/utils/overclock_timers'; -import { createCommitId } from 'test_helpers/factories/commit_id'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import * as ideHelper from './helpers/ide_helper'; -import startWebIDE from './helpers/start'; - -describe('WebIDE', () => { - useOverclockTimers(); - - let vm; - let container; - - beforeEach(() => { - stubPerformanceWebAPI(); - setHTMLFixture('
    '); - container = document.querySelector('.webide-container'); - }); - - afterEach(() => { - vm.$destroy(); - resetHTMLFixture(); - }); - - it('user commits changes', async () => { - vm = startWebIDE(container); - - await ideHelper.createFile('foo/bar/test.txt', 'Lorem ipsum dolar sit'); - await ideHelper.deleteFile('foo/bar/.gitkeep'); - await ideHelper.commit(); - - const commitId = createCommitId(1); - const commitShortId = commitId.slice(0, 8); - - await waitForText('All changes are committed'); - await waitForText(commitShortId); - - expect(mockServer.db.branches.findBy({ name: 'master' }).commit).toMatchObject({ - short_id: commitShortId, - id: commitId, - message: 'Update foo/bar/test.txt\nDeleted foo/bar/.gitkeep', - __actions: [ - { - action: 'create', - content: 'Lorem ipsum dolar sit\n', - encoding: 'text', - file_path: 'foo/bar/test.txt', - last_commit_id: '', - }, - { - action: 'delete', - encoding: 'text', - file_path: 'foo/bar/.gitkeep', - }, - ], - }); - }); - - it('user commits changes to new branch', async () => { - vm = startWebIDE(container); - - expect(window.location.pathname).toBe('/-/ide/project/gitlab-test/lorem-ipsum/tree/master/-/'); - - await ideHelper.updateFile('README.md', 'Lorem dolar si amit\n'); - await ideHelper.commit({ newBranch: true, newMR: false, newBranchName: 'test-hello-world' }); - - await waitForText('All changes are committed'); - - // Wait for IDE to load new commit - await waitForText('10000000', document.querySelector('.ide-status-bar')); - - // It's important that the new branch is now in the route - expect(window.location.pathname).toBe( - '/-/ide/project/gitlab-test/lorem-ipsum/blob/test-hello-world/-/README.md', - ); - }); - - it('user adds file that starts with +', async () => { - vm = startWebIDE(container); - - await ideHelper.createFile('+test', 'Hello world!'); - await ideHelper.openFile('+test'); - - // Wait for monaco things - await waitForPromises(); - - // Assert that +test is the only open tab - const tabs = Array.from(document.querySelectorAll('.multi-file-tab')); - expect(tabs.map((x) => x.textContent.trim())).toEqual(['+test']); - }); - - describe('editor info', () => { - let statusBar; - let editor; - - beforeEach(async () => { - vm = startWebIDE(container); - - await ideHelper.openFile('README.md'); - editor = await ideHelper.waitForMonacoEditor(); - - statusBar = ideHelper.getStatusBar(); - }); - - it('shows line position and type', () => { - expect(statusBar).toHaveText('1:1'); - expect(statusBar).toHaveText('markdown'); - }); - - it('persists viewer', async () => { - const checkText = async (text) => { - const el = await waitForText(text); - expect(el).toHaveText(text); - }; - - const markdownPreview = 'test preview_markdown result'; - mockServer.post('/:namespace/:project/-/preview_markdown', () => ({ - body: markdownPreview, - })); - - await ideHelper.openFile('README.md'); - ideHelper.clickPreviewMarkdown(); - - await checkText(markdownPreview); - - // Need to wait for monaco editor to load so it doesn't through errors on dispose - await ideHelper.openFile('.gitignore'); - await ideHelper.waitForEditorModelChange(editor); - await ideHelper.openFile('README.md'); - await ideHelper.waitForEditorModelChange(editor); - - await checkText(markdownPreview); - }); - - describe('when editor position changes', () => { - beforeEach(async () => { - editor.setPosition({ lineNumber: 4, column: 10 }); - await nextTick(); - }); - - it('shows new line position', () => { - expect(statusBar).not.toHaveText('1:1'); - expect(statusBar).toHaveText('4:10'); - }); - - it('updates after rename', async () => { - await ideHelper.renameFile('README.md', 'READMEZ.txt'); - await ideHelper.waitForEditorModelChange(editor); - await nextTick(); - - expect(statusBar).toHaveText('1:1'); - expect(statusBar).toHaveText('plaintext'); - }); - - it('persists position after opening then rename', async () => { - await ideHelper.openFile('files/js/application.js'); - await ideHelper.waitForEditorModelChange(editor); - await ideHelper.renameFile('README.md', 'READING_RAINBOW.md'); - await ideHelper.openFile('READING_RAINBOW.md'); - await ideHelper.waitForEditorModelChange(editor); - - expect(statusBar).toHaveText('4:10'); - expect(statusBar).toHaveText('markdown'); - }); - - it('persists position after closing', async () => { - await ideHelper.closeFile('README.md'); - await ideHelper.openFile('README.md'); - await ideHelper.waitForMonacoEditor(); - await nextTick(); - - expect(statusBar).toHaveText('4:10'); - expect(statusBar).toHaveText('markdown'); - }); - }); - }); -}); diff --git a/spec/frontend_integration/ide/user_opens_file_spec.js b/spec/frontend_integration/ide/user_opens_file_spec.js deleted file mode 100644 index 93c9fff309f..00000000000 --- a/spec/frontend_integration/ide/user_opens_file_spec.js +++ /dev/null @@ -1,90 +0,0 @@ -import { screen } from '@testing-library/dom'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import { useOverclockTimers } from 'test_helpers/utils/overclock_timers'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import * as ideHelper from './helpers/ide_helper'; -import startWebIDE from './helpers/start'; - -describe('IDE: User opens a file in the Web IDE', () => { - useOverclockTimers(); - - let vm; - let container; - - beforeEach(async () => { - stubPerformanceWebAPI(); - setHTMLFixture('
    '); - container = document.querySelector('.webide-container'); - - vm = startWebIDE(container); - - await screen.findByText('README'); // wait for file tree to load - }); - - afterEach(() => { - vm.$destroy(); - resetHTMLFixture(); - }); - - describe('user opens a directory', () => { - beforeEach(async () => { - await ideHelper.openFile('files/images'); - await screen.findByText('logo-white.png'); - }); - - it('expands directory in the left sidebar', () => { - expect(ideHelper.getFilesList()).toEqual( - expect.arrayContaining(['html', 'js', 'images', 'logo-white.png']), - ); - }); - }); - - describe('user opens a text file', () => { - beforeEach(async () => { - await ideHelper.openFile('README.md'); - await ideHelper.waitForTabToOpen('README.md'); - }); - - it('opens the file in monaco editor', async () => { - expect(await ideHelper.getEditorValue()).toContain('Sample repo for testing gitlab features'); - }); - - describe('user switches to review mode', () => { - beforeEach(() => { - ideHelper.switchLeftSidebarTab('Review'); - }); - - it('shows diff editor', async () => { - expect(await ideHelper.findMonacoDiffEditor()).toBeDefined(); - }); - }); - }); - - describe('user opens an image file', () => { - beforeEach(async () => { - await ideHelper.openFile('files/images/logo-white.png'); - await ideHelper.waitForTabToOpen('logo-white.png'); - }); - - it('opens image viewer for the file', async () => { - const viewer = await screen.findByTestId('image-viewer'); - const img = viewer.querySelector('img'); - - expect(img.src).toContain('logo-white.png'); - }); - }); - - describe('user opens a binary file', () => { - beforeEach(async () => { - await ideHelper.openFile('Gemfile.zip'); - await ideHelper.waitForTabToOpen('Gemfile.zip'); - }); - - it('opens image viewer for the file', async () => { - const downloadButton = await screen.findByText('Download'); - - expect(downloadButton.getAttribute('download')).toEqual('Gemfile.zip'); - expect(downloadButton.getAttribute('href')).toContain('/raw/'); - }); - }); -}); diff --git a/spec/frontend_integration/ide/user_opens_ide_spec.js b/spec/frontend_integration/ide/user_opens_ide_spec.js deleted file mode 100644 index 2f89b3c0612..00000000000 --- a/spec/frontend_integration/ide/user_opens_ide_spec.js +++ /dev/null @@ -1,164 +0,0 @@ -import { screen } from '@testing-library/dom'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import { useOverclockTimers } from 'test_helpers/utils/overclock_timers'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import * as ideHelper from './helpers/ide_helper'; -import startWebIDE from './helpers/start'; - -describe('IDE: User opens IDE', () => { - useOverclockTimers(); - - let vm; - let container; - - beforeEach(() => { - stubPerformanceWebAPI(); - - setHTMLFixture('
    '); - container = document.querySelector('.webide-container'); - }); - - afterEach(() => { - vm.$destroy(); - resetHTMLFixture(); - }); - - it('shows loading indicator while the IDE is loading', () => { - vm = startWebIDE(container); - - expect(container.querySelectorAll('.multi-file-loading-container')).toHaveLength(3); - }); - - describe('when the project is empty', () => { - beforeEach(() => { - vm = startWebIDE(container, { isRepoEmpty: true }); - }); - - it('shows "No files" in the left sidebar', async () => { - expect(await screen.findByText('No files')).toBeDefined(); - }); - - it('shows a "New file" button', () => { - const buttons = screen.queryAllByTitle('New file'); - - expect(buttons.map((x) => x.tagName)).toContain('BUTTON'); - }); - }); - - describe('when the file tree is loaded', () => { - beforeEach(async () => { - vm = startWebIDE(container); - - await screen.findByText('README'); // wait for file tree to load - }); - - it('shows a list of files in the left sidebar', () => { - expect(ideHelper.getFilesList()).toEqual( - expect.arrayContaining(['README', 'LICENSE', 'CONTRIBUTING.md']), - ); - }); - - it('shows empty state in the main editor window', async () => { - expect( - await screen.findByText( - "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes.", - ), - ).toBeDefined(); - }); - - it('shows commit button in disabled state', async () => { - const button = await screen.findByTestId('begin-commit-button'); - - expect(button.getAttribute('disabled')).toBeDefined(); - }); - - it('shows branch/MR dropdown with master selected', async () => { - const dropdown = await screen.findByTestId('ide-nav-dropdown'); - - expect(dropdown.textContent).toContain('master'); - }); - }); - - describe('a path to a text file is present in the URL', () => { - beforeEach(async () => { - vm = startWebIDE(container, { path: 'README.md' }); - - await ideHelper.waitForTabToOpen('README.md'); - }); - - it('opens the file and its contents are shown in Monaco', async () => { - expect(await ideHelper.getEditorValue()).toContain('Sample repo for testing gitlab features'); - }); - }); - - describe('a path to a binary file is present in the URL', () => { - beforeEach(async () => { - vm = startWebIDE(container, { path: 'Gemfile.zip' }); - - await ideHelper.waitForTabToOpen('Gemfile.zip'); - }); - - it('shows download viewer', async () => { - const downloadButton = await screen.findByText('Download'); - - expect(downloadButton.getAttribute('download')).toEqual('Gemfile.zip'); - expect(downloadButton.getAttribute('href')).toContain('/raw/'); - }); - }); - - describe('a path to an image is present in the URL', () => { - beforeEach(async () => { - vm = startWebIDE(container, { path: 'files/images/logo-white.png' }); - - await ideHelper.waitForTabToOpen('logo-white.png'); - }); - - it('shows image viewer', async () => { - const viewer = await screen.findByTestId('image-viewer'); - const img = viewer.querySelector('img'); - - expect(img.src).toContain('logo-white.png'); - }); - }); - - describe('path in URL is a directory', () => { - beforeEach(async () => { - vm = startWebIDE(container, { path: 'files/images' }); - - // wait for folders in left sidebar to be expanded - await screen.findByText('images'); - }); - - it('expands folders in the left sidebar', () => { - expect(ideHelper.getFilesList()).toEqual( - expect.arrayContaining(['files', 'images', 'logo-white.png', 'logo-black.png']), - ); - }); - - it('shows empty state in the main editor window', async () => { - expect( - await screen.findByText( - "Select a file from the left sidebar to begin editing. Afterwards, you'll be able to commit your changes.", - ), - ).toBeDefined(); - }); - }); - - describe("a file for path in url doesn't exist in the repo", () => { - beforeEach(async () => { - vm = startWebIDE(container, { path: 'abracadabra/hocus-focus.txt' }); - - await ideHelper.waitForTabToOpen('hocus-focus.txt'); - }); - - it('create new folders and file in the left sidebar', () => { - expect(ideHelper.getFilesList()).toEqual( - expect.arrayContaining(['abracadabra', 'hocus-focus.txt']), - ); - }); - - it('creates a blank new file', async () => { - expect(await ideHelper.getEditorValue()).toEqual('\n'); - }); - }); -}); diff --git a/spec/frontend_integration/ide/user_opens_mr_spec.js b/spec/frontend_integration/ide/user_opens_mr_spec.js deleted file mode 100644 index 4e90cef6016..00000000000 --- a/spec/frontend_integration/ide/user_opens_mr_spec.js +++ /dev/null @@ -1,64 +0,0 @@ -import { basename } from 'path'; -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import { getMergeRequests, getMergeRequestWithChanges } from 'test_helpers/fixtures'; -import { useOverclockTimers } from 'test_helpers/utils/overclock_timers'; -import { stubPerformanceWebAPI } from 'helpers/performance'; -import * as ideHelper from './helpers/ide_helper'; -import startWebIDE from './helpers/start'; - -const getRelevantChanges = () => - getMergeRequestWithChanges().changes.filter((x) => !x.deleted_file); - -describe('IDE: User opens Merge Request', () => { - useOverclockTimers(); - - let vm; - let container; - let changes; - - beforeEach(async () => { - stubPerformanceWebAPI(); - - const [{ iid: mrId }] = getMergeRequests(); - - changes = getRelevantChanges(); - - setHTMLFixture('
    '); - container = document.querySelector('.webide-container'); - - vm = startWebIDE(container, { mrId }); - - const editor = await ideHelper.waitForMonacoEditor(); - await ideHelper.waitForEditorModelChange(editor); - }); - - afterEach(() => { - vm.$destroy(); - resetHTMLFixture(); - }); - - const findAllTabs = () => Array.from(document.querySelectorAll('.multi-file-tab')); - const findAllTabsData = () => - findAllTabs().map((el) => ({ - title: el.getAttribute('title'), - text: el.textContent.trim(), - })); - - it('shows first change as active in file tree', async () => { - const firstPath = changes[0].new_path; - const row = await ideHelper.findAndTraverseToPath(firstPath); - - expect(row).toHaveClass('is-open'); - expect(row).toHaveClass('is-active'); - }); - - it('opens other changes', () => { - // We only show first 10 changes - const expectedTabs = changes.slice(0, 10).map((x) => ({ - title: `${ideHelper.getBaseRoute()}/-/${x.new_path}/`, - text: basename(x.new_path), - })); - - expect(findAllTabsData()).toEqual(expectedTabs); - }); -}); diff --git a/spec/lib/gitlab/auth/session_expire_from_init_enforcer_spec.rb b/spec/lib/gitlab/auth/session_expire_from_init_enforcer_spec.rb new file mode 100644 index 00000000000..9f1da906ba1 --- /dev/null +++ b/spec/lib/gitlab/auth/session_expire_from_init_enforcer_spec.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::SessionExpireFromInitEnforcer, feature_category: :system_access do + let(:request_double) { instance_double(ActionDispatch::Request) } + let(:session) { {} } + let(:warden) { instance_double(Warden::Proxy, request: request_double, session: session) } + let(:opts) do + { + scope: :user + } + end + + let(:instance) { described_class.new(warden, opts) } + + describe '.enabled?' do + subject(:is_enabled) { described_class.enabled? } + + it { is_expected.to be(false) } + + context 'when session_expire_from_init setting is enabled' do + before do + stub_application_setting(session_expire_from_init: true) + end + + it { is_expected.to be(true) } + + context 'and session_expire_from_init FF is disabled' do + before do + stub_feature_flags(session_expire_from_init: false) + end + + it { is_expected.to be(false) } + end + end + end + + describe '.session_expires_at', :freeze_time do + subject(:session_expires_at) { described_class.session_expires_at(session) } + + let(:signed_in_at) { nil } + let(:session) do + { + 'warden.user.user.session' => { + described_class::SESSION_NAMESPACE => { + 'signed_in_at' => signed_in_at + } + } + } + end + + before do + stub_application_setting( + session_expire_from_init: true, + session_expire_delay: 5 + ) + end + + it { is_expected.to eq(0) } + + context 'when session has sign in data set by enforcer' do + let(:signed_in_at) { Time.current.utc.to_i - 4.minutes } + + it { is_expected.to eq(Time.current.utc.to_i + 1.minute) } + end + + context 'when session has expired' do + let(:signed_in_at) { Time.current.utc.to_i - 6.minutes } + + it { is_expected.to eq(Time.current.utc.to_i - 1.minute) } + end + + context 'when session has no data' do + let(:session) { {} } + + it { is_expected.to eq(0) } + end + end + + describe '#set_login_time', :freeze_time do + subject(:set_login_time) { instance.set_login_time } + + before do + stub_application_setting( + session_expire_from_init: true, + session_expire_delay: 5 + ) + end + + it 'sets signed_in_at session info' do + set_login_time + + expect(session[described_class::SESSION_NAMESPACE]['signed_in_at']).to eq(Time.current.utc.to_i) + end + + context 'when not enabled' do + before do + stub_application_setting( + session_expire_from_init: false, + session_expire_delay: 5 + ) + end + + it 'does not set signed_in_at session info' do + set_login_time + + expect(session).to be_empty + end + end + + context 'when session_expire_from_init FF is disabled' do + before do + stub_feature_flags(session_expire_from_init: false) + end + + it 'does not set signed_in_at session info' do + set_login_time + + expect(session).to be_empty + end + end + end + + describe '#enforce!', :freeze_time do + subject(:enforce) { instance.enforce! } + + let(:devise_proxy) { instance_double(Devise::Hooks::Proxy) } + + before do + stub_application_setting( + session_expire_from_init: true, + session_expire_delay: 5 + ) + allow(instance).to receive(:proxy).and_return(devise_proxy) + end + + it 'does not throw' do + expect { enforce }.not_to raise_error + end + + context 'when session contains signed_in_at info' do + let(:session) do + { + described_class::SESSION_NAMESPACE => { + 'signed_in_at' => Time.current.utc.to_i - 5.minutes - 1 + } + } + end + + it 'throws :warden error' do + expect(devise_proxy).to receive(:sign_out) + + expect { enforce }.to throw_symbol(:warden) + end + + context 'when session_expire_from_init FF is disabled' do + before do + stub_feature_flags(session_expire_from_init: false) + end + + it 'does not throw :warden symbol' do + expect(devise_proxy).not_to receive(:sign_out) + + expect { enforce }.not_to throw_symbol + end + end + + context 'when session has not expired yet' do + let(:session) do + { + described_class::SESSION_NAMESPACE => { + 'signed_in_at' => Time.current.utc.to_i - 3.minutes + } + } + end + + it 'does not throw :warden symbol' do + expect(devise_proxy).not_to receive(:sign_out) + + expect { enforce }.not_to throw_symbol + end + end + + context 'when session_expire_from_init is not enabled' do + before do + stub_application_setting( + session_expire_from_init: false, + session_expire_delay: 5 + ) + end + + it 'does not throw :warden symbol' do + expect(devise_proxy).not_to receive(:sign_out) + + expect { enforce }.not_to throw_symbol + end + end + end + end +end diff --git a/spec/migrations/20250417141927_add_pub_purl_type_to_application_setting_spec.rb b/spec/migrations/20250417141927_add_pub_purl_type_to_application_setting_spec.rb new file mode 100644 index 00000000000..b1319bf3308 --- /dev/null +++ b/spec/migrations/20250417141927_add_pub_purl_type_to_application_setting_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe AddPubPurlTypeToApplicationSetting, feature_category: :software_composition_analysis do + let(:settings) { table(:application_settings) } + + describe "#up" do + it 'updates setting' do + settings.create!(package_metadata_purl_types: [1, 2, 4, 5, 9, 10]) + + disable_migrations_output do + migrate! + end + + expect(settings.last.package_metadata_purl_types).to eq([1, 2, 4, 5, 9, 10, 17]) + end + end + + describe "#down" do + context 'with default value' do + it 'updates setting' do + settings.create!(package_metadata_purl_types: [1, 2, 4, 5, 9, 10, 15, 17]) + + disable_migrations_output do + migrate! + schema_migrate_down! + end + + expect(settings.last.package_metadata_purl_types).to eq([1, 2, 4, 5, 9, 10, 15]) + end + end + end +end diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb index ca7ec709662..c8cbb659772 100644 --- a/spec/models/active_session_spec.rb +++ b/spec/models/active_session_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe ActiveSession, :clean_gitlab_redis_sessions, feature_category: :system_access do - include SessionHelpers - let(:lookup_key) { described_class.lookup_key_name(user.id) } let(:user) do create(:user).tap do |user| @@ -287,83 +285,6 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_sessions, feature_category: :s ) end end - - context 'when session_expire_from_init is set' do - before do - stub_application_setting(session_expire_from_init: true, session_expire_delay: 10080) - end - - it 'sets a new redis entry with the same ttl' do - described_class.set(user, request) - key = described_class.key_name(user.id, rack_session.private_id) - - # Sanity check for default ttl - expect(get_ttl(key)).to be_within(10).of(Settings.gitlab['session_expire_delay'] * 60) - - # Mock passage of time - expire(key, 4.days.to_i) - - # passage of time with no additional session access should have no effect - expect(get_ttl(key)).to be_within(20).of(4.days.to_i) - - described_class.set(user, request) - - # ensure active session reset did not modify ttl - expect(get_ttl(key)).to be_within(20).of(4.days.to_i) - end - - context 'and session_expire_from_init FF is disabled' do - before do - stub_feature_flags(session_expire_from_init: false) - end - - it 'updates ttl using session max lifetime' do - described_class.set(user, request) - key = described_class.key_name(user.id, rack_session.private_id) - - # Sanity check for default ttl - expect(get_ttl(key)).to be_within(10).of(Settings.gitlab['session_expire_delay'] * 60) - - # Mock passage of time - expire(key, 4.days.to_i) - - # Manual expiration setting should be respected by redis - expect(get_ttl(key)).to be_within(10).of(4.days.to_i) - - # recreate session, should reset ttl to setting_expire_session_delay * 60 - described_class.set(user, request) - - # ensure active session reset did not modify ttl - expect(get_ttl(key)).to be_within(10).of(Settings.gitlab['session_expire_delay'] * 60) - end - end - end - - context 'when session expire from init is disabled' do - before do - stub_application_setting(session_expire_from_init: false, session_expire_delay: 10080) - end - - it 'restarts session duration' do - described_class.set(user, request) - key = described_class.key_name(user.id, rack_session.private_id) - - # Sanity check for default ttl - expect(get_ttl(key)).to be_within(10).of(Settings.gitlab['session_expire_delay'] * 60) - - # Mock passage of time - expire(key, 4.days.to_i) - - # Manual expiration setting should be respected by redis - expect(get_ttl(key)).to be_within(10).of(4.days.to_i) - - # recreate session, should reset ttl to setting_expire_session_delay * 60 - described_class.set(user, request) - - # ensure active session reset did not modify ttl - expect(get_ttl(key)).to be_within(10).of(Settings.gitlab['session_expire_delay'] * 60) - end - end end describe '.destroy_session' do diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index eaefde414b8..3a0b673f59a 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -2104,6 +2104,32 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do end end + describe '#session_expire_from_init_enabled?' do + subject(:session_expire_from_init_enabled) { setting.session_expire_from_init_enabled? } + + before do + setting.session_expire_from_init = true + end + + it { is_expected.to be true } + + context 'when session_expire_from_init is set to false' do + before do + setting.session_expire_from_init = false + end + + it { is_expected.to be false } + end + + context 'when session_expire_from_init FF is disabled' do + before do + stub_feature_flags(session_expire_from_init: false) + end + + it { is_expected.to be false } + end + end + context 'for security txt content' do it { is_expected.to validate_length_of(:security_txt_content).is_at_most(2048) } end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 98fa4bcdf60..93f5d7baf3f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2685,6 +2685,34 @@ RSpec.describe User, feature_category: :user_profile do expect(user.remember_created_at).to be_nil end end + + context 'when session_expire_from_init is enabled' do + before do + stub_application_setting(remember_me_enabled: true, session_expire_from_init: true) + end + + it 'does not set rememberable attributes' do + expect(user.remember_created_at).to be_nil + + user.remember_me! + + expect(user.remember_created_at).to be_nil + end + + context 'when session_expire_from_init FF is disabled' do + before do + stub_feature_flags(session_expire_from_init: false) + end + + it 'sets rememberable attributes' do + expect(user.remember_created_at).to be_nil + + user.remember_me! + + expect(user.remember_created_at).not_to be_nil + end + end + end end describe '#invalidate_all_remember_tokens!' do diff --git a/spec/views/layouts/_flash.html.haml_spec.rb b/spec/views/layouts/_flash.html.haml_spec.rb index d88977b194a..cd203ec4992 100644 --- a/spec/views/layouts/_flash.html.haml_spec.rb +++ b/spec/views/layouts/_flash.html.haml_spec.rb @@ -54,4 +54,12 @@ RSpec.describe 'layouts/_flash' do expect(rendered).to have_selector(".flash-container.#{flash_container_no_margin_class}") end end + + describe 'with Warden timedout flash message' do + let(:flash) { { 'timedout' => true } } + + it 'does not render info box with the word true in it' do + expect(rendered).not_to include('true') + end + end end diff --git a/yarn.lock b/yarn.lock index fc690b954a7..8e49084d0c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1424,7 +1424,7 @@ resolved "https://registry.yarnpkg.com/@gitlab/fonts/-/fonts-1.3.0.tgz#df89c1bb6714e4a8a5d3272568aa4de7fb337267" integrity sha512-DoMUIN3DqjEn7wvcxBg/b7Ite5fTdF5EmuOZoBRo2j0UBGweDXmNBi+9HrTZs4cBU660dOxcf1hATFcG3npbPg== -"@gitlab/noop@^1.0.1": +"@gitlab/noop@^1.0.1", jackspeak@^3.1.2, "jackspeak@npm:@gitlab/noop@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454" integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q== @@ -1448,10 +1448,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.128.0.tgz#fa4691b33b7f6d2b34fee7a77bcd8accc4690355" integrity sha512-W+2LWpbztWksJ62hhZ2A4pPOksofHUUazqEYPqQP7DMAtt7n7NzfdNeCNy5i1BULEeBaBzvOAGBUObx4FbQj3w== -"@gitlab/ui@113.0.0": - version "113.0.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-113.0.0.tgz#7aa38a44fdf339698045b442eb1b1dc050b27162" - integrity sha512-y05BSHfD9EAmj7unBJLwgOAubJ45kh0h9NXuXnsO+i99R0O1p6d4aa1KTakNY7uaYmiiwsl75o0NYjVDlfp7Ng== +"@gitlab/ui@113.2.0": + version "113.2.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-113.2.0.tgz#857382702cc7efa423a41f16e3c7e1feef32e589" + integrity sha512-ZiHeIidTyH2iNAAEw/P+SiSbuV/c/rf8Bg0XCktD/cfbuqTMCYYQJII9G6wF95c0Q1dhx96OZGfzUSdkWWLkbA== dependencies: "@floating-ui/dom" "1.4.3" echarts "^5.3.2" @@ -9442,11 +9442,6 @@ iterall@^1.2.1: resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== -jackspeak@^3.1.2, "jackspeak@npm:@gitlab/noop@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454" - integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q== - jed@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"