diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 09:08:11 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-20 09:08:11 +0000 |
commit | 1f5a2543e4daf21dd98d8ff0514781c403445c81 (patch) | |
tree | 22af0594a5de457ffb346c2259f9d30c3fd5479f | |
parent | 9bded6fb2268204757c35540fadef8e1b6351249 (diff) | |
download | gitlab-ce-1f5a2543e4daf21dd98d8ff0514781c403445c81.tar.gz |
Add latest changes from gitlab-org/gitlab@master
70 files changed, 1017 insertions, 615 deletions
diff --git a/CHANGELOG-EE.md b/CHANGELOG-EE.md index b48dce65463..c63254a24b4 100644 --- a/CHANGELOG-EE.md +++ b/CHANGELOG-EE.md @@ -1,5 +1,9 @@ Please view this file on the master branch, on stable branches it's out of date. +## 12.10.6 (2020-05-15) + +- No changes. + ## 12.10.5 (2020-05-13) ### Fixed (1 change) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b0b8ba9c0a..2b9f26ef35a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 12.10.6 (2020-05-15) + +### Fixed (5 changes) + +- Fix duplicate index removal on ci_pipelines.project_id. !31043 +- Fix 500 on creating an invalid domains and verification. !31190 +- Fix incorrect number of errors returned when querying sentry errors. !31252 +- Add instance column to services table if it's missing. !31631 +- Fix incorrect regex used in FileUploader#extract_dynamic_path. !32271 + + ## 12.10.5 (2020-05-13) ### Added (1 change) diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue index 7ff3271394d..f4014e3cc55 100644 --- a/app/assets/javascripts/design_management/pages/design/index.vue +++ b/app/assets/javascripts/design_management/pages/design/index.vue @@ -156,6 +156,9 @@ export default { }, mounted() { Mousetrap.bind('esc', this.closeDesign); + this.trackEvent(); + // We need to reset the active discussion when opening a new design + this.updateActiveDiscussion(); }, beforeDestroy() { Mousetrap.unbind('esc', this.closeDesign); @@ -279,23 +282,6 @@ export default { }); }, }, - beforeRouteEnter(to, from, next) { - next(vm => { - vm.trackEvent(); - }); - }, - beforeRouteUpdate(to, from, next) { - this.trackEvent(); - this.closeCommentForm(); - // We need to reset the active discussion when opening a new design - this.updateActiveDiscussion(); - next(); - }, - beforeRouteLeave(to, from, next) { - // We need to reset the active discussion when moving to design list view - this.updateActiveDiscussion(); - next(); - }, createImageDiffNoteMutation, DESIGNS_ROUTE_NAME, }; diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index 7d419bc3ded..922c800009f 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -318,6 +318,6 @@ export default { </li> </ol> </div> - <router-view /> + <router-view :key="$route.fullPath" /> </div> </template> diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index 5656bfc4707..e3fb0650e45 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -128,6 +128,7 @@ export default { <no-preview-viewer v-else-if="noPreview" /> <diff-viewer v-else + :diff-file="diffFile" :diff-mode="diffMode" :diff-viewer-mode="diffViewerMode" :new-path="diffFile.new_path" diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index 40e1aec42ed..9269dacd582 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -61,3 +61,22 @@ export const DIFFS_PER_PAGE = 20; export const DIFF_COMPARE_BASE_VERSION_INDEX = -1; export const DIFF_COMPARE_HEAD_VERSION_INDEX = -2; + +// State machine states +export const STATE_IDLING = 'idle'; +export const STATE_LOADING = 'loading'; +export const STATE_ERRORED = 'errored'; + +// State machine transitions +export const TRANSITION_LOAD_START = 'LOAD_START'; +export const TRANSITION_LOAD_ERROR = 'LOAD_ERROR'; +export const TRANSITION_LOAD_SUCCEED = 'LOAD_SUCCEED'; +export const TRANSITION_ACKNOWLEDGE_ERROR = 'ACKNOWLEDGE_ERROR'; + +export const RENAMED_DIFF_TRANSITIONS = { + [`${STATE_IDLING}:${TRANSITION_LOAD_START}`]: STATE_LOADING, + [`${STATE_LOADING}:${TRANSITION_LOAD_ERROR}`]: STATE_ERRORED, + [`${STATE_LOADING}:${TRANSITION_LOAD_SUCCEED}`]: STATE_IDLING, + [`${STATE_ERRORED}:${TRANSITION_LOAD_START}`]: STATE_LOADING, + [`${STATE_ERRORED}:${TRANSITION_ACKNOWLEDGE_ERROR}`]: STATE_IDLING, +}; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 1975d6996a5..085dd34179e 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -656,11 +656,6 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines }); dispatch('startRenderDiffsQueue'); - }) - .catch(error => { - dispatch('receiveFullDiffError', diffFile.file_path); - - throw error; }); } diff --git a/app/assets/javascripts/ide/commit_icon.js b/app/assets/javascripts/ide/commit_icon.js new file mode 100644 index 00000000000..4984b5bb91d --- /dev/null +++ b/app/assets/javascripts/ide/commit_icon.js @@ -0,0 +1,11 @@ +import { commitItemIconMap } from './constants'; + +export default file => { + if (file.deleted) { + return commitItemIconMap.deleted; + } else if (file.tempFile && !file.prevPath) { + return commitItemIconMap.addition; + } + + return commitItemIconMap.modified; +}; diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index e70e251c117..5ae44c0d9a8 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -4,7 +4,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import { viewerTypes } from '../../constants'; -import { getCommitIconMap } from '../../utils'; +import getCommitIconMap from '../../commit_icon'; export default { components: { diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index e32b5ac7bdc..479def40013 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -297,6 +297,3 @@ export * from './actions/tree'; export * from './actions/file'; export * from './actions/project'; export * from './actions/merge_request'; - -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js index 5d0a8570906..d148d0cd993 100644 --- a/app/assets/javascripts/ide/stores/getters.js +++ b/app/assets/javascripts/ide/stores/getters.js @@ -161,6 +161,3 @@ export const canCreateMergeRequests = (state, getters) => export const canPushCode = (state, getters) => Boolean(getters.findProjectPermissions(state.currentProjectId)[PERMISSION_PUSH_CODE]); - -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/clientside/actions.js b/app/assets/javascripts/ide/stores/modules/clientside/actions.js index eb3bcdff2ae..2bebf8b90ce 100644 --- a/app/assets/javascripts/ide/stores/modules/clientside/actions.js +++ b/app/assets/javascripts/ide/stores/modules/clientside/actions.js @@ -8,5 +8,4 @@ export const pingUsage = ({ rootGetters }) => { return axios.post(url); }; -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; +export default pingUsage; diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js index 592c7e15918..65e2726a976 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/actions.js +++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js @@ -234,6 +234,3 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo window.dispatchEvent(new Event('resize')); }); }; - -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index 413c4b0110d..37f887bcf0a 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -59,6 +59,3 @@ export const shouldDisableNewMrOption = (state, getters, rootState, rootGetters) export const shouldCreateMR = (state, getters) => state.shouldCreateMR && !getters.shouldDisableNewMrOption; - -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js index 59ead8a3dcf..6b2c929cd44 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js @@ -117,6 +117,3 @@ export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => { dispatch('discardFileChanges', file.path, { root: true }); } }; - -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pane/actions.js b/app/assets/javascripts/ide/stores/modules/pane/actions.js index a8fcdf539ec..b7cff368fe4 100644 --- a/app/assets/javascripts/ide/stores/modules/pane/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pane/actions.js @@ -25,6 +25,3 @@ export const open = ({ state, commit }, view) => { export const close = ({ commit }) => { commit(types.SET_OPEN, false); }; - -// prevent babel-plugin-rewire from generating an invalid default during karma tests -export default () => {}; diff --git a/app/assets/javascripts/ide/utils.js b/app/assets/javascripts/ide/utils.js index 1ea2b199237..9d14b7f7d48 100644 --- a/app/assets/javascripts/ide/utils.js +++ b/app/assets/javascripts/ide/utils.js @@ -1,4 +1,3 @@ -import { commitItemIconMap } from './constants'; import { languages } from 'monaco-editor'; import { flatten } from 'lodash'; @@ -53,16 +52,6 @@ export function isTextFile(content, mimeType, fileName) { return asciiRegex.test(content); } -export const getCommitIconMap = file => { - if (file.deleted) { - return commitItemIconMap.deleted; - } else if (file.tempFile && !file.prevPath) { - return commitItemIconMap.addition; - } - - return commitItemIconMap.modified; -}; - export const createPathWithExt = p => { const ext = p.lastIndexOf('.') >= 0 ? p.substring(p.lastIndexOf('.') + 1) : ''; diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 2bbf9ef9d78..5acb9ebc043 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -22,6 +22,8 @@ export default (props = {}) => { props: { ...el.dataset, currentDashboard, + customMetricsAvailable: parseBoolean(el.dataset.customMetricsAvailable), + prometheusAlertsAvailable: parseBoolean(el.dataset.prometheusAlertsAvailable), hasMetrics: parseBoolean(el.dataset.hasMetrics), ...props, }, diff --git a/app/assets/javascripts/monitoring/monitoring_bundle_with_alerts.js b/app/assets/javascripts/monitoring/monitoring_bundle_with_alerts.js deleted file mode 100644 index afe5ee0938d..00000000000 --- a/app/assets/javascripts/monitoring/monitoring_bundle_with_alerts.js +++ /dev/null @@ -1,13 +0,0 @@ -import { parseBoolean } from '~/lib/utils/common_utils'; -import initCeBundle from '~/monitoring/monitoring_bundle'; - -export default () => { - const el = document.getElementById('prometheus-graphs'); - - if (el && el.dataset) { - initCeBundle({ - customMetricsAvailable: parseBoolean(el.dataset.customMetricsAvailable), - prometheusAlertsAvailable: parseBoolean(el.dataset.prometheusAlertsAvailable), - }); - } -}; diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index cd5cfc09ea0..8897b54fac7 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -116,6 +116,7 @@ export default { </div> <div v-else> <diff-viewer + :diff-file="discussion.diff_file" :diff-mode="diffMode" :diff-viewer-mode="diffViewerMode" :new-path="discussion.diff_file.new_path" diff --git a/app/assets/javascripts/pages/projects/environments/metrics/index.js b/app/assets/javascripts/pages/projects/environments/metrics/index.js index 31ec4e29ad2..0b644780ad4 100644 --- a/app/assets/javascripts/pages/projects/environments/metrics/index.js +++ b/app/assets/javascripts/pages/projects/environments/metrics/index.js @@ -1,3 +1,3 @@ -import monitoringBundle from '~/monitoring/monitoring_bundle_with_alerts'; +import monitoringBundle from '~/monitoring/monitoring_bundle'; document.addEventListener('DOMContentLoaded', monitoringBundle); diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index 60e41a16854..7431b7e9ed4 100644 --- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -1,7 +1,7 @@ <script> import { GlTooltipDirective } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; -import { getCommitIconMap } from '~/ide/utils'; +import getCommitIconMap from '~/ide/commit_icon'; import { __ } from '~/locale'; export default { diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue index bf3c3666300..a2fe19f9672 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue @@ -7,6 +7,10 @@ import ModeChanged from './viewers/mode_changed.vue'; export default { props: { + diffFile: { + type: Object, + required: true, + }, diffMode: { type: String, required: true, @@ -92,6 +96,7 @@ export default { <div v-if="viewer" class="diff-file preview-container"> <component :is="viewer" + :diff-file="diffFile" :diff-mode="diffMode" :new-path="fullNewPath" :old-path="fullOldPath" diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue index 5c1ea59b471..eba6dd4d14c 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue @@ -1,3 +1,108 @@ +<script> +import { mapActions } from 'vuex'; +import { GlAlert, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; + +import { __ } from '~/locale'; +import { + TRANSITION_LOAD_START, + TRANSITION_LOAD_ERROR, + TRANSITION_LOAD_SUCCEED, + TRANSITION_ACKNOWLEDGE_ERROR, + STATE_IDLING, + STATE_LOADING, + STATE_ERRORED, + RENAMED_DIFF_TRANSITIONS, +} from '~/diffs/constants'; +import { truncateSha } from '~/lib/utils/text_utility'; + +export default { + STATE_LOADING, + STATE_ERRORED, + TRANSITIONS: RENAMED_DIFF_TRANSITIONS, + uiText: { + showLink: __('Show file contents'), + commitLink: __('View file @ %{commitSha}'), + description: __('File renamed with no changes.'), + loadError: __('Unable to load file contents. Try again later.'), + }, + components: { + GlAlert, + GlLink, + GlLoadingIcon, + GlSprintf, + }, + props: { + diffFile: { + type: Object, + required: true, + }, + }, + data: () => ({ + state: STATE_IDLING, + }), + computed: { + shortSha() { + return truncateSha(this.diffFile.content_sha); + }, + canLoadFullDiff() { + return this.diffFile.alternate_viewer.name === 'text'; + }, + }, + methods: { + ...mapActions('diffs', ['switchToFullDiffFromRenamedFile']), + transition(transitionEvent) { + const key = `${this.state}:${transitionEvent}`; + + if (this.$options.TRANSITIONS[key]) { + this.state = this.$options.TRANSITIONS[key]; + } + }, + is(state) { + return this.state === state; + }, + switchToFull() { + this.transition(TRANSITION_LOAD_START); + + this.switchToFullDiffFromRenamedFile({ diffFile: this.diffFile }) + .then(() => { + this.transition(TRANSITION_LOAD_SUCCEED); + }) + .catch(() => { + this.transition(TRANSITION_LOAD_ERROR); + }); + }, + clickLink(event) { + if (this.canLoadFullDiff) { + event.preventDefault(); + + this.switchToFull(); + } + }, + dismissError() { + this.transition(TRANSITION_ACKNOWLEDGE_ERROR); + }, + }, +}; +</script> + <template> - <div class="nothing-here-block">{{ __('File moved') }}</div> + <div class="nothing-here-block"> + <gl-loading-icon v-if="is($options.STATE_LOADING)" /> + <template v-else> + <gl-alert + v-show="is($options.STATE_ERRORED)" + class="gl-mb-5 gl-text-left" + variant="danger" + @dismiss="dismissError" + >{{ $options.uiText.loadError }}</gl-alert + > + <span test-id="plaintext">{{ $options.uiText.description }}</span> + <gl-link :href="diffFile.view_path" @click="clickLink"> + <span v-if="canLoadFullDiff">{{ $options.uiText.showLink }}</span> + <gl-sprintf v-else :message="$options.uiText.commitLink"> + <template #commitSha>{{ shortSha }}</template> + </gl-sprintf> + </gl-link> + </template> + </div> </template> diff --git a/changelogs/unreleased/216728-actionview-template-error-undefined-method-pages_domain-for-pagesd.yml b/changelogs/unreleased/216728-actionview-template-error-undefined-method-pages_domain-for-pagesd.yml deleted file mode 100644 index d34e092c9ee..00000000000 --- a/changelogs/unreleased/216728-actionview-template-error-undefined-method-pages_domain-for-pagesd.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix 500 on creating an invalid domains and verification -merge_request: 31190 -author: -type: fixed diff --git a/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml b/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml deleted file mode 100644 index dd36d52f1c4..00000000000 --- a/changelogs/unreleased/216851-graphql-externallypaginatedarrayconnection-can-return-incorrect-nu.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix incorrect number of errors returned when querying sentry errors -merge_request: 31252 -author: -type: fixed diff --git a/changelogs/unreleased/216970-add-instance-to-service-if-missing.yml b/changelogs/unreleased/216970-add-instance-to-service-if-missing.yml deleted file mode 100644 index d8fcbb8f587..00000000000 --- a/changelogs/unreleased/216970-add-instance-to-service-if-missing.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Add instance column to services table if it's missing -merge_request: 31631 -author: -type: fixed diff --git a/changelogs/unreleased/217602-file-uploads-on-local-storage-with-nil-secret-in-the-db-are-broken.yml b/changelogs/unreleased/217602-file-uploads-on-local-storage-with-nil-secret-in-the-db-are-broken.yml deleted file mode 100644 index 48e585b59e8..00000000000 --- a/changelogs/unreleased/217602-file-uploads-on-local-storage-with-nil-secret-in-the-db-are-broken.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix incorrect regex used in FileUploader#extract_dynamic_path -merge_request: 32271 -author: -type: fixed diff --git a/changelogs/unreleased/cat-duplicate-ci-pipelines-index-215790.yml b/changelogs/unreleased/cat-duplicate-ci-pipelines-index-215790.yml deleted file mode 100644 index a21cafe5e14..00000000000 --- a/changelogs/unreleased/cat-duplicate-ci-pipelines-index-215790.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Fix duplicate index removal on ci_pipelines.project_id -merge_request: 31043 -author: -type: fixed diff --git a/changelogs/unreleased/upgrade-renamed-diff-show-full-diff-for-renamed-files.yml b/changelogs/unreleased/upgrade-renamed-diff-show-full-diff-for-renamed-files.yml new file mode 100644 index 00000000000..3a9cc4c383a --- /dev/null +++ b/changelogs/unreleased/upgrade-renamed-diff-show-full-diff-for-renamed-files.yml @@ -0,0 +1,5 @@ +--- +title: Add a link to the `renamed` viewer to fully expand the renamed file (if it's text) +merge_request: 28448 +author: +type: added diff --git a/danger/telemetry/Dangerfile b/danger/telemetry/Dangerfile index c18a15fcb03..b749bd3b80b 100644 --- a/danger/telemetry/Dangerfile +++ b/danger/telemetry/Dangerfile @@ -1,12 +1,11 @@ # frozen_string_literal: true TELEMETRY_CHANGED_FILES_MESSAGE = <<~MSG -This merge request includes changes for which a review from the Data team and Telemetry team is recommended. -Please reach out to @gitlab-org/growth/telemetry/engineers group for a review. -MSG +For the following files, a review from the [Data team and Telemetry team](https://gitlab.com/groups/gitlab-org/growth/telemetry/engineers/-/group_members?with_inherited_permissions=exclude) is recommended +Please check the ~telemetry [guide](https://docs.gitlab.com/ee/development/telemetry/usage_ping.html) and reach out to @gitlab-org/growth/telemetry/engineers group for a review. + +%<changed_files>s -USAGE_DATA_FILES_MESSAGE = <<~MSG -For the following files, a review from the [Data team and Telemetry team](https://gitlab.com/groups/gitlab-org/growth/telemetry/engineers/-/group_members?with_inherited_permissions=exclude) is recommended: MSG tracking_files = [ @@ -16,7 +15,7 @@ tracking_files = [ 'spec/helpers/tracking_helper_spec.rb', 'app/assets/javascripts/tracking.js', 'spec/frontend/tracking_spec.js' - ] +] usage_data_changed_files = git.modified_files.grep(%r{usage_data}) snowplow_events_changed_files = git.modified_files & tracking_files @@ -24,9 +23,7 @@ snowplow_events_changed_files = git.modified_files & tracking_files changed_files = (usage_data_changed_files + snowplow_events_changed_files) if changed_files.any? - warn format(TELEMETRY_CHANGED_FILES_MESSAGE) - - markdown(USAGE_DATA_FILES_MESSAGE + helper.markdown_list(changed_files)) + warn format(TELEMETRY_CHANGED_FILES_MESSAGE, changed_files: helper.markdown_list(changed_files)) telemetry_labels = ['telemetry'] telemetry_labels << 'telemetry::review pending' unless helper.mr_has_labels?('telemetry::reviewed') diff --git a/doc/administration/geo/disaster_recovery/index.md b/doc/administration/geo/disaster_recovery/index.md index 9f7afad353b..3bf58798dca 100644 --- a/doc/administration/geo/disaster_recovery/index.md +++ b/doc/administration/geo/disaster_recovery/index.md @@ -125,10 +125,10 @@ Note the following when promoting a secondary: previously for the **secondary** node. 1. If successful, the **secondary** node has now been promoted to the **primary** node. -#### Promoting a **secondary** node with HA +#### Promoting a **secondary** node with multiple servers The `gitlab-ctl promote-to-primary-node` command cannot be used yet in -conjunction with High Availability or with multiple machines, as it can only +conjunction with multiple servers, as it can only perform changes on a **secondary** with only a single machine. Instead, you must do this manually. diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md index 293414a6e5e..37571486447 100644 --- a/doc/administration/geo/replication/troubleshooting.md +++ b/doc/administration/geo/replication/troubleshooting.md @@ -568,7 +568,7 @@ is displayed if you attempt to run this command on a primary node. ### Message: `sudo: gitlab-pg-ctl: command not found` When -[promoting a **secondary** node with HA](../disaster_recovery/index.md#promoting-a-secondary-node-with-ha), +[promoting a **secondary** node with multiple servers](../disaster_recovery/index.md#promoting-a-secondary-node-with-multiple-servers), you need to run the `gitlab-pg-ctl` command to promote the PostgreSQL read-replica database. diff --git a/doc/administration/operations/puma.md b/doc/administration/operations/puma.md index af559cf00e9..a53f83da8c0 100644 --- a/doc/administration/operations/puma.md +++ b/doc/administration/operations/puma.md @@ -10,7 +10,8 @@ Unicorn unless explicitly specified not to. ## Why switch to Puma? Puma has a multi-thread architecture which uses less memory than a multi-process -application server like Unicorn. +application server like Unicorn. On GitLab.com, we saw a 40% reduction in memory +consumption. Most Rails applications requests normally include a proportion of I/O wait time. During I/O wait time MRI Ruby will release the GVL (Global VM Lock) to other threads. @@ -18,9 +19,14 @@ Multi-threaded Puma can therefore still serve more requests than a single proces ## Configuring Puma to replace Unicorn -If you are currently running Unicorn and would like to switch to Puma, server configuration -will _not_ carry over automatically. For details on matching Unicorn configuration settings with -the Puma equivalent, where applicable, see [Converting Unicorn settings to Puma](https://docs.gitlab.com/omnibus/settings/puma.html#converting-unicorn-settings-to-puma). +Beginning with GitLab 13.0, Puma is the default application server. We plan to remove support for +Unicorn in GitLab 14.0. + +When switching to Puma, Unicorn server configuration +will _not_ carry over automatically, due to differences between the two application servers. + +For Omnibus-based deployments, see [Configuring Puma Settings](https://docs.gitlab.com/omnibus/settings/puma.html#configuring-puma-settings). For Helm +based deployments, see the [Webservice Chart documentation](https://docs.gitlab.com/charts/charts/gitlab/webservice/index.html). ## Performance caveat when using Puma with Rugged diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 09ad5f9afd7..174fff1c7cd 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -204,7 +204,7 @@ Omnibus GitLab defaults to the recommended Puma settings. Regardless of installa tune the Puma settings. If you're using Omnibus GitLab, see [Puma settings](https://docs.gitlab.com/omnibus/settings/puma.html) -for instructions on changing the Puma settings. +for instructions on changing the Puma settings. If you are using the GitLab Helm chart, see the [Webservice chart](https://docs.gitlab.com/charts/charts/gitlab/webservice/index.html). ### Puma workers diff --git a/doc/user/admin_area/merge_requests_approvals.md b/doc/user/admin_area/merge_requests_approvals.md index d0092b1eaf0..0c7beadad48 100644 --- a/doc/user/admin_area/merge_requests_approvals.md +++ b/doc/user/admin_area/merge_requests_approvals.md @@ -19,22 +19,6 @@ To enable merge request approval rules for an instance: GitLab administrators can later override these settings in a project’s settings. -## Merge request controls **(PREMIUM)** - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/207250) in GitLab 13.0. - -Merge request approval settings, by default, are inherited by all projects in an instance. - -However, organizations with regulated projects may also have unregulated projects -that should not inherit these same controls. - -Project-level merge request approval rules can now be edited by administrators. -Project owners and maintainers can still view project-level merge request approval rules. - -In upcoming releases, we plan to provide a more holistic experience to scope instance-level merge request settings. -For more information, review our plans to provide custom [approval settings for compliance- -labeled projects](https://gitlab.com/gitlab-org/gitlab/-/issues/213601). - ## Available rules Merge request approval rules that can be set at an instance level are: diff --git a/doc/user/packages/conan_repository/index.md b/doc/user/packages/conan_repository/index.md index a56a67d4959..288ee9bf1c9 100644 --- a/doc/user/packages/conan_repository/index.md +++ b/doc/user/packages/conan_repository/index.md @@ -1,3 +1,9 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + # GitLab Conan Repository **(PREMIUM)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/8248) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.6. diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index 5e642e1e21c..737470ccd62 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -1,3 +1,9 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + # GitLab Container Registry > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4040) in GitLab 8.8. diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md index be9710053dd..8df492cbc16 100644 --- a/doc/user/packages/dependency_proxy/index.md +++ b/doc/user/packages/dependency_proxy/index.md @@ -1,3 +1,9 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + # Dependency Proxy **(PREMIUM ONLY)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/7934) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 11.11. diff --git a/doc/user/packages/index.md b/doc/user/packages/index.md index 132a64d99a3..3209207156b 100644 --- a/doc/user/packages/index.md +++ b/doc/user/packages/index.md @@ -1,3 +1,9 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + # GitLab Package Registry GitLab Packages allows organizations to utilize GitLab as a private repository diff --git a/doc/user/packages/maven_repository/index.md b/doc/user/packages/maven_repository/index.md index 51e62dc871e..0e71dcacd5f 100644 --- a/doc/user/packages/maven_repository/index.md +++ b/doc/user/packages/maven_repository/index.md @@ -1,3 +1,9 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + # GitLab Maven Repository **(PREMIUM)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/5811) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.3. diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md index b909646431b..ec96456b1ec 100644 --- a/doc/user/packages/npm_registry/index.md +++ b/doc/user/packages/npm_registry/index.md @@ -1,3 +1,9 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + # GitLab NPM Registry **(PREMIUM)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/5934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.7. diff --git a/doc/user/packages/nuget_repository/index.md b/doc/user/packages/nuget_repository/index.md index d9efb3239a8..057fbb852cb 100644 --- a/doc/user/packages/nuget_repository/index.md +++ b/doc/user/packages/nuget_repository/index.md @@ -1,3 +1,9 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + # GitLab NuGet Repository **(PREMIUM)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/20050) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.8. diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md index 979f7a3c966..6f6609d82b1 100644 --- a/doc/user/packages/pypi_repository/index.md +++ b/doc/user/packages/pypi_repository/index.md @@ -1,3 +1,9 @@ +--- +stage: Package +group: Package +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + # GitLab PyPi Repository **(PREMIUM)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208747) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.10. diff --git a/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md b/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md index fdcb1049ef7..145ebb4a96a 100644 --- a/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md +++ b/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md @@ -82,6 +82,10 @@ to expand the entire file. ![Incrementally expand merge request diffs](img/incrementally_expand_merge_request_diffs_v12_2.png) +[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/205401) in GitLab 13.1, when viewing a +merge request's **Changes** tab, if a certain file was only renamed, you can expand it to see the +entire content by clicking **Show file contents**. + ### Ignore whitespace changes in Merge Request diff view If you click the **Hide whitespace changes** button, you can see the diff diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 14afe67bbfd..e59e767376c 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -9491,6 +9491,9 @@ msgstr "" msgid "File name" msgstr "" +msgid "File renamed with no changes." +msgstr "" + msgid "File sync capacity" msgstr "" @@ -14058,6 +14061,12 @@ msgstr "" msgid "NetworkPolicies|Kubernetes error: %{error}" msgstr "" +msgid "NetworkPolicies|Policy %{policyName} was successfully changed" +msgstr "" + +msgid "NetworkPolicies|Something went wrong, failed to update policy" +msgstr "" + msgid "NetworkPolicies|Something went wrong, unable to fetch policies" msgstr "" @@ -19671,6 +19680,9 @@ msgstr "" msgid "Show file browser" msgstr "" +msgid "Show file contents" +msgstr "" + msgid "Show latest version" msgstr "" @@ -23061,6 +23073,9 @@ msgstr "" msgid "Unable to generate new instance ID" msgstr "" +msgid "Unable to load file contents. Try again later." +msgstr "" + msgid "Unable to load the diff" msgstr "" @@ -23983,6 +23998,9 @@ msgstr[1] "" msgid "View file @ " msgstr "" +msgid "View file @ %{commitSha}" +msgstr "" + msgid "View full dashboard" msgstr "" diff --git a/spec/frontend/__mocks__/lodash/throttle.js b/spec/frontend/__mocks__/lodash/throttle.js new file mode 100644 index 00000000000..aef391afd0c --- /dev/null +++ b/spec/frontend/__mocks__/lodash/throttle.js @@ -0,0 +1,4 @@ +// Similar to `lodash/debounce`, `lodash/throttle` also causes flaky specs. +// See `./debounce.js` for more details. + +export default fn => fn; diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js index 0f4afa5e288..d6488d3837a 100644 --- a/spec/frontend/design_management/router_spec.js +++ b/spec/frontend/design_management/router_spec.js @@ -33,6 +33,7 @@ function factory(routeArg) { design: { loading: true }, permissions: { loading: true }, }, + mutate: jest.fn(), }, }, }); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 3fba661da44..b6038462170 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -1255,7 +1255,6 @@ describe('DiffsStoreActions', () => { describe('switchToFullDiffFromRenamedFile', () => { const SUCCESS_URL = 'fakehost/context.success'; - const ERROR_URL = 'fakehost/context.error'; const testFilePath = 'testpath'; const updatedViewerName = 'testviewer'; const preparedLine = { prepared: 'in-a-test' }; @@ -1311,27 +1310,6 @@ describe('DiffsStoreActions', () => { }, ); }); - - describe('error', () => { - beforeEach(() => { - renamedFile = { ...testFile, context_lines_path: ERROR_URL }; - mock.onGet(ERROR_URL).reply(500); - }); - - it('dispatches the error handling action', () => { - const rejected = testAction( - switchToFullDiffFromRenamedFile, - { diffFile: renamedFile }, - null, - [], - [{ type: 'receiveFullDiffError', payload: testFilePath }], - ); - - return rejected.catch(error => - expect(error).toEqual(new Error('Request failed with status code 500')), - ); - }); - }); }); describe('setFileCollapsed', () => { diff --git a/spec/frontend/helpers/dom_shims/element_scroll_to.js b/spec/frontend/helpers/dom_shims/element_scroll_to.js new file mode 100644 index 00000000000..68f8a115865 --- /dev/null +++ b/spec/frontend/helpers/dom_shims/element_scroll_to.js @@ -0,0 +1,6 @@ +Element.prototype.scrollTo = jest.fn().mockImplementation(function scrollTo(x, y) { + this.scrollLeft = x; + this.scrollTop = y; + + this.dispatchEvent(new Event('scroll')); +}); diff --git a/spec/frontend/helpers/dom_shims/index.js b/spec/frontend/helpers/dom_shims/index.js index 17a2090d2f1..ed1c708c444 100644 --- a/spec/frontend/helpers/dom_shims/index.js +++ b/spec/frontend/helpers/dom_shims/index.js @@ -1,5 +1,6 @@ import './element_scroll_into_view'; import './element_scroll_by'; +import './element_scroll_to'; import './form_element'; import './get_client_rects'; import './inner_text'; diff --git a/spec/frontend/ide/commit_icon_spec.js b/spec/frontend/ide/commit_icon_spec.js new file mode 100644 index 00000000000..90b8e34497c --- /dev/null +++ b/spec/frontend/ide/commit_icon_spec.js @@ -0,0 +1,45 @@ +import { commitItemIconMap } from '~/ide/constants'; +import { decorateData } from '~/ide/stores/utils'; +import getCommitIconMap from '~/ide/commit_icon'; + +const createFile = (name = 'name', id = name, type = '', parent = null) => + decorateData({ + id, + type, + icon: 'icon', + url: 'url', + name, + path: parent ? `${parent.path}/${name}` : name, + parentPath: parent ? parent.path : '', + lastCommit: {}, + }); + +describe('getCommitIconMap', () => { + let entry; + + beforeEach(() => { + entry = createFile('Entry item'); + }); + + it('renders "deleted" icon for deleted entries', () => { + entry.deleted = true; + expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.deleted); + }); + + it('renders "addition" icon for temp entries', () => { + entry.tempFile = true; + expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.addition); + }); + + it('renders "modified" icon for newly-renamed entries', () => { + entry.prevPath = 'foo/bar'; + entry.tempFile = false; + expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified); + }); + + it('renders "modified" icon even for temp entries if they are newly-renamed', () => { + entry.prevPath = 'foo/bar'; + entry.tempFile = true; + expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified); + }); +}); diff --git a/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js index 53508f52b2f..d6ea8b9a4bd 100644 --- a/spec/javascripts/ide/components/commit_sidebar/message_field_spec.js +++ b/spec/frontend/ide/components/commit_sidebar/message_field_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import createComponent from 'spec/helpers/vue_mount_component_helper'; +import createComponent from 'helpers/vue_mount_component_helper'; import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue'; describe('IDE commit message field', () => { @@ -54,7 +54,7 @@ describe('IDE commit message field', () => { }); it('emits input event on input', () => { - spyOn(vm, '$emit'); + jest.spyOn(vm, '$emit').mockImplementation(); const textarea = vm.$el.querySelector('textarea'); textarea.value = 'testing'; @@ -160,7 +160,7 @@ describe('IDE commit message field', () => { .then(() => { expect(vm.scrollTop).toBe(50); expect(vm.$el.querySelector('.highlights').style.transform).toBe( - 'translate3d(0px, -50px, 0px)', + 'translate3d(0, -50px, 0)', ); }) .then(done) diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js new file mode 100644 index 00000000000..8f3815d5aab --- /dev/null +++ b/spec/frontend/ide/components/jobs/detail_spec.js @@ -0,0 +1,187 @@ +import Vue from 'vue'; +import JobDetail from '~/ide/components/jobs/detail.vue'; +import { createStore } from '~/ide/stores'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { jobs } from '../../mock_data'; +import { TEST_HOST } from 'helpers/test_constants'; + +describe('IDE jobs detail view', () => { + let vm; + + const createComponent = () => { + const store = createStore(); + + store.state.pipelines.detailJob = { + ...jobs[0], + isLoading: true, + output: 'testing', + rawPath: `${TEST_HOST}/raw`, + }; + + return createComponentWithStore(Vue.extend(JobDetail), store); + }; + + beforeEach(() => { + vm = createComponent(); + + jest.spyOn(vm, 'fetchJobTrace').mockResolvedValue(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('mounted', () => { + beforeEach(() => { + vm = vm.$mount(); + }); + + it('calls fetchJobTrace', () => { + expect(vm.fetchJobTrace).toHaveBeenCalled(); + }); + + it('scrolls to bottom', () => { + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalled(); + }); + + it('renders job output', () => { + expect(vm.$el.querySelector('.bash').textContent).toContain('testing'); + }); + + it('renders empty message output', done => { + vm.$store.state.pipelines.detailJob.output = ''; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.bash').textContent).toContain('No messages were logged'); + + done(); + }); + }); + + it('renders loading icon', () => { + expect(vm.$el.querySelector('.build-loader-animation')).not.toBe(null); + expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe(''); + }); + + it('hides output when loading', () => { + expect(vm.$el.querySelector('.bash')).not.toBe(null); + expect(vm.$el.querySelector('.bash').style.display).toBe('none'); + }); + + it('hide loading icon when isLoading is false', done => { + vm.$store.state.pipelines.detailJob.isLoading = false; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('none'); + + done(); + }); + }); + + it('resets detailJob when clicking header button', () => { + jest.spyOn(vm, 'setDetailJob').mockImplementation(); + + vm.$el.querySelector('.btn').click(); + + expect(vm.setDetailJob).toHaveBeenCalledWith(null); + }); + + it('renders raw path link', () => { + expect(vm.$el.querySelector('.controllers-buttons').getAttribute('href')).toBe( + `${TEST_HOST}/raw`, + ); + }); + }); + + describe('scroll buttons', () => { + beforeEach(() => { + vm = createComponent(); + jest.spyOn(vm, 'fetchJobTrace').mockResolvedValue(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it.each` + fnName | btnName | scrollPos + ${'scrollDown'} | ${'down'} | ${0} + ${'scrollUp'} | ${'up'} | ${1} + `('triggers $fnName when clicking $btnName button', ({ fnName, scrollPos }) => { + jest.spyOn(vm, fnName).mockImplementation(); + + vm = vm.$mount(); + + vm.scrollPos = scrollPos; + + return vm.$nextTick().then(() => { + vm.$el.querySelector('.btn-scroll:not([disabled])').click(); + expect(vm[fnName]).toHaveBeenCalled(); + }); + }); + }); + + describe('scrollDown', () => { + beforeEach(() => { + vm = vm.$mount(); + + jest.spyOn(vm.$refs.buildTrace, 'scrollTo').mockImplementation(); + }); + + it('scrolls build trace to bottom', () => { + jest.spyOn(vm.$refs.buildTrace, 'scrollHeight', 'get').mockReturnValue(1000); + + vm.scrollDown(); + + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 1000); + }); + }); + + describe('scrollUp', () => { + beforeEach(() => { + vm = vm.$mount(); + + jest.spyOn(vm.$refs.buildTrace, 'scrollTo').mockImplementation(); + }); + + it('scrolls build trace to top', () => { + vm.scrollUp(); + + expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 0); + }); + }); + + describe('scrollBuildLog', () => { + beforeEach(() => { + vm = vm.$mount(); + jest.spyOn(vm.$refs.buildTrace, 'scrollTo').mockImplementation(); + jest.spyOn(vm.$refs.buildTrace, 'offsetHeight', 'get').mockReturnValue(100); + jest.spyOn(vm.$refs.buildTrace, 'scrollHeight', 'get').mockReturnValue(200); + }); + + it('sets scrollPos to bottom when at the bottom', () => { + jest.spyOn(vm.$refs.buildTrace, 'scrollTop', 'get').mockReturnValue(100); + + vm.scrollBuildLog(); + + expect(vm.scrollPos).toBe(1); + }); + + it('sets scrollPos to top when at the top', () => { + jest.spyOn(vm.$refs.buildTrace, 'scrollTop', 'get').mockReturnValue(0); + vm.scrollPos = 1; + + vm.scrollBuildLog(); + + expect(vm.scrollPos).toBe(0); + }); + + it('resets scrollPos when not at top or bottom', () => { + jest.spyOn(vm.$refs.buildTrace, 'scrollTop', 'get').mockReturnValue(10); + + vm.scrollBuildLog(); + + expect(vm.scrollPos).toBe(''); + }); + }); +}); diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js index d909a5e478e..795ded35d20 100644 --- a/spec/frontend/ide/components/pipelines/list_spec.js +++ b/spec/frontend/ide/components/pipelines/list_spec.js @@ -6,7 +6,7 @@ import List from '~/ide/components/pipelines/list.vue'; import JobsList from '~/ide/components/jobs/list.vue'; import Tab from '~/vue_shared/components/tabs/tab.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import { pipelines } from '../../../../javascripts/ide/mock_data'; +import { pipelines } from 'jest/ide/mock_data'; import IDEServices from '~/ide/services'; const localVue = createLocalVue(); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js index 8db29011da7..af29e172332 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/frontend/ide/components/repo_editor_spec.js @@ -7,13 +7,13 @@ import repoEditor from '~/ide/components/repo_editor.vue'; import Editor from '~/ide/lib/editor'; import { leftSidebarViews, FILE_VIEW_MODE_EDITOR, FILE_VIEW_MODE_PREVIEW } from '~/ide/constants'; import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; -import setTimeoutPromise from '../../helpers/set_timeout_promise_helper'; +import waitForPromises from 'helpers/wait_for_promises'; import { file, resetStore } from '../helpers'; describe('RepoEditor', () => { let vm; - beforeEach(done => { + beforeEach(() => { const f = { ...file(), viewMode: FILE_VIEW_MODE_EDITOR, @@ -45,12 +45,12 @@ describe('RepoEditor', () => { Vue.set(vm.$store.state.entries, f.path, f); - spyOn(vm, 'getFileData').and.returnValue(Promise.resolve()); - spyOn(vm, 'getRawFileData').and.returnValue(Promise.resolve()); + jest.spyOn(vm, 'getFileData').mockResolvedValue(); + jest.spyOn(vm, 'getRawFileData').mockResolvedValue(); vm.$mount(); - Vue.nextTick(() => setTimeout(done)); + return vm.$nextTick(); }); afterEach(() => { @@ -161,7 +161,7 @@ describe('RepoEditor', () => { .then(() => { vm.$el.querySelectorAll('.ide-mode-tabs .nav-links a')[1].click(); }) - .then(setTimeoutPromise) + .then(waitForPromises) .then(() => { expect(vm.$el.querySelector('.preview-container').innerHTML).toContain( '<p>testing 123</p>', @@ -186,7 +186,7 @@ describe('RepoEditor', () => { describe('createEditorInstance', () => { it('calls createInstance when viewer is editor', done => { - spyOn(vm.editor, 'createInstance'); + jest.spyOn(vm.editor, 'createInstance').mockImplementation(); vm.createEditorInstance(); @@ -200,7 +200,7 @@ describe('RepoEditor', () => { it('calls createDiffInstance when viewer is diff', done => { vm.$store.state.viewer = 'diff'; - spyOn(vm.editor, 'createDiffInstance'); + jest.spyOn(vm.editor, 'createDiffInstance').mockImplementation(); vm.createEditorInstance(); @@ -214,7 +214,7 @@ describe('RepoEditor', () => { it('calls createDiffInstance when viewer is a merge request diff', done => { vm.$store.state.viewer = 'mrdiff'; - spyOn(vm.editor, 'createDiffInstance'); + jest.spyOn(vm.editor, 'createDiffInstance').mockImplementation(); vm.createEditorInstance(); @@ -228,7 +228,7 @@ describe('RepoEditor', () => { describe('setupEditor', () => { it('creates new model', () => { - spyOn(vm.editor, 'createModel').and.callThrough(); + jest.spyOn(vm.editor, 'createModel'); Editor.editorInstance.modelManager.dispose(); @@ -239,7 +239,7 @@ describe('RepoEditor', () => { }); it('attaches model to editor', () => { - spyOn(vm.editor, 'attachModel').and.callThrough(); + jest.spyOn(vm.editor, 'attachModel'); Editor.editorInstance.modelManager.dispose(); @@ -251,7 +251,7 @@ describe('RepoEditor', () => { it('attaches model to merge request editor', () => { vm.$store.state.viewer = 'mrdiff'; vm.file.mrChange = true; - spyOn(vm.editor, 'attachMergeRequestModel'); + jest.spyOn(vm.editor, 'attachMergeRequestModel').mockImplementation(); Editor.editorInstance.modelManager.dispose(); @@ -263,7 +263,7 @@ describe('RepoEditor', () => { it('does not attach model to merge request editor when not a MR change', () => { vm.$store.state.viewer = 'mrdiff'; vm.file.mrChange = false; - spyOn(vm.editor, 'attachMergeRequestModel'); + jest.spyOn(vm.editor, 'attachMergeRequestModel').mockImplementation(); Editor.editorInstance.modelManager.dispose(); @@ -273,7 +273,7 @@ describe('RepoEditor', () => { }); it('adds callback methods', () => { - spyOn(vm.editor, 'onPositionChange').and.callThrough(); + jest.spyOn(vm.editor, 'onPositionChange'); Editor.editorInstance.modelManager.dispose(); @@ -286,7 +286,7 @@ describe('RepoEditor', () => { it('updates state when model content changed', done => { vm.model.setValue('testing 123\n'); - setTimeout(() => { + setImmediate(() => { expect(vm.file.content).toBe('testing 123\n'); done(); @@ -294,7 +294,7 @@ describe('RepoEditor', () => { }); it('sets head model as staged file', () => { - spyOn(vm.editor, 'createModel').and.callThrough(); + jest.spyOn(vm.editor, 'createModel'); Editor.editorInstance.modelManager.dispose(); @@ -310,8 +310,8 @@ describe('RepoEditor', () => { describe('editor updateDimensions', () => { beforeEach(() => { - spyOn(vm.editor, 'updateDimensions').and.callThrough(); - spyOn(vm.editor, 'updateDiffView'); + jest.spyOn(vm.editor, 'updateDimensions'); + jest.spyOn(vm.editor, 'updateDiffView').mockImplementation(); }); it('calls updateDimensions when panelResizing is false', done => { @@ -381,7 +381,7 @@ describe('RepoEditor', () => { describe('when files view mode is preview', () => { beforeEach(done => { - spyOn(vm.editor, 'updateDimensions'); + jest.spyOn(vm.editor, 'updateDimensions').mockImplementation(); vm.file.viewMode = FILE_VIEW_MODE_PREVIEW; vm.$nextTick(done); }); @@ -392,19 +392,12 @@ describe('RepoEditor', () => { }); describe('when file view mode changes to editor', () => { - beforeEach(done => { + it('should update dimensions', () => { vm.file.viewMode = FILE_VIEW_MODE_EDITOR; - // one tick to trigger watch - vm.$nextTick() - // another tick needed until we can update dimensions - .then(() => vm.$nextTick()) - .then(done) - .catch(done.fail); - }); - - it('should update dimensions', () => { - expect(vm.editor.updateDimensions).toHaveBeenCalled(); + return vm.$nextTick().then(() => { + expect(vm.editor.updateDimensions).toHaveBeenCalled(); + }); }); }); }); @@ -412,8 +405,8 @@ describe('RepoEditor', () => { describe('initEditor', () => { beforeEach(() => { vm.file.tempFile = false; - spyOn(vm.editor, 'createInstance'); - spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true); + jest.spyOn(vm.editor, 'createInstance').mockImplementation(); + jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true); }); it('does not fetch file information for temp entries', done => { @@ -459,12 +452,12 @@ describe('RepoEditor', () => { describe('updates on file changes', () => { beforeEach(() => { - spyOn(vm, 'initEditor'); + jest.spyOn(vm, 'initEditor').mockImplementation(); }); it('calls removePendingTab when old file is pending', done => { - spyOnProperty(vm, 'shouldHideEditor').and.returnValue(true); - spyOn(vm, 'removePendingTab'); + jest.spyOn(vm, 'shouldHideEditor', 'get').mockReturnValue(true); + jest.spyOn(vm, 'removePendingTab').mockImplementation(); vm.file.pending = true; diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js index ce09cf51ac5..cb4eebd97d9 100644 --- a/spec/javascripts/ide/stores/actions/merge_request_spec.js +++ b/spec/frontend/ide/stores/actions/merge_request_spec.js @@ -1,7 +1,8 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import store from '~/ide/stores'; -import actions, { +import createFlash from '~/flash'; +import { getMergeRequestData, getMergeRequestChanges, getMergeRequestVersions, @@ -14,6 +15,8 @@ import { resetStore } from '../../helpers'; const TEST_PROJECT = 'abcproject'; const TEST_PROJECT_ID = 17; +jest.mock('~/flash'); + describe('IDE store merge request actions', () => { let mock; @@ -41,7 +44,7 @@ describe('IDE store merge request actions', () => { describe('base case', () => { beforeEach(() => { - spyOn(service, 'getProjectMergeRequests').and.callThrough(); + jest.spyOn(service, 'getProjectMergeRequests'); mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, mockData); }); @@ -66,7 +69,7 @@ describe('IDE store merge request actions', () => { .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) .then(() => { expect(store.state.projects.abcproject.mergeRequests).toEqual({ - '2': jasmine.objectContaining(mrData), + '2': expect.objectContaining(mrData), }); done(); }) @@ -99,7 +102,7 @@ describe('IDE store merge request actions', () => { describe('no merge requests for branch available case', () => { beforeEach(() => { - spyOn(service, 'getProjectMergeRequests').and.callThrough(); + jest.spyOn(service, 'getProjectMergeRequests'); mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, []); }); @@ -122,16 +125,11 @@ describe('IDE store merge request actions', () => { }); it('flashes message, if error', done => { - const flashSpy = spyOnDependency(actions, 'flash'); - store .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' }) - .then(() => { - fail('Expected getMergeRequestsForBranch to throw an error'); - }) .catch(() => { - expect(flashSpy).toHaveBeenCalled(); - expect(flashSpy.calls.argsFor(0)[0]).toEqual('Error fetching merge requests for bar'); + expect(createFlash).toHaveBeenCalled(); + expect(createFlash.mock.calls[0][0]).toBe('Error fetching merge requests for bar'); }) .then(done) .catch(done.fail); @@ -142,7 +140,7 @@ describe('IDE store merge request actions', () => { describe('getMergeRequestData', () => { describe('success', () => { beforeEach(() => { - spyOn(service, 'getProjectMergeRequestData').and.callThrough(); + jest.spyOn(service, 'getProjectMergeRequestData'); mock .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/) @@ -181,7 +179,7 @@ describe('IDE store merge request actions', () => { }); it('dispatches error action', done => { - const dispatch = jasmine.createSpy('dispatch'); + const dispatch = jest.fn(); getMergeRequestData( { @@ -195,7 +193,7 @@ describe('IDE store merge request actions', () => { .catch(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { text: 'An error occurred while loading the merge request.', - action: jasmine.any(Function), + action: expect.any(Function), actionText: 'Please try again', actionPayload: { projectId: TEST_PROJECT, @@ -217,7 +215,7 @@ describe('IDE store merge request actions', () => { describe('success', () => { beforeEach(() => { - spyOn(service, 'getProjectMergeRequestChanges').and.callThrough(); + jest.spyOn(service, 'getProjectMergeRequestChanges'); mock .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/) @@ -254,7 +252,7 @@ describe('IDE store merge request actions', () => { }); it('dispatches error action', done => { - const dispatch = jasmine.createSpy('dispatch'); + const dispatch = jest.fn(); getMergeRequestChanges( { @@ -268,7 +266,7 @@ describe('IDE store merge request actions', () => { .catch(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { text: 'An error occurred while loading the merge request changes.', - action: jasmine.any(Function), + action: expect.any(Function), actionText: 'Please try again', actionPayload: { projectId: TEST_PROJECT, @@ -293,7 +291,7 @@ describe('IDE store merge request actions', () => { mock .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/) .reply(200, [{ id: 789 }]); - spyOn(service, 'getProjectMergeRequestVersions').and.callThrough(); + jest.spyOn(service, 'getProjectMergeRequestVersions'); }); it('calls getProjectMergeRequestVersions service method', done => { @@ -324,7 +322,7 @@ describe('IDE store merge request actions', () => { }); it('dispatches error action', done => { - const dispatch = jasmine.createSpy('dispatch'); + const dispatch = jest.fn(); getMergeRequestVersions( { @@ -338,7 +336,7 @@ describe('IDE store merge request actions', () => { .catch(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { text: 'An error occurred while loading the merge request version data.', - action: jasmine.any(Function), + action: expect.any(Function), actionText: 'Please try again', actionPayload: { projectId: TEST_PROJECT, @@ -400,7 +398,7 @@ describe('IDE store merge request actions', () => { const originalDispatch = store.dispatch; - spyOn(store, 'dispatch').and.callFake((type, payload) => { + jest.spyOn(store, 'dispatch').mockImplementation((type, payload) => { switch (type) { case 'getMergeRequestData': return Promise.resolve(testMergeRequest); @@ -415,7 +413,7 @@ describe('IDE store merge request actions', () => { return originalDispatch(type, payload); } }); - spyOn(service, 'getFileData').and.callFake(() => + jest.spyOn(service, 'getFileData').mockImplementation(() => Promise.resolve({ headers: {}, }), @@ -425,7 +423,7 @@ describe('IDE store merge request actions', () => { it('dispatches actions for merge request data', done => { openMergeRequest({ state: store.state, dispatch: store.dispatch, getters: mockGetters }, mr) .then(() => { - expect(store.dispatch.calls.allArgs()).toEqual([ + expect(store.dispatch.mock.calls).toEqual([ ['getMergeRequestData', mr], ['setCurrentBranchId', testMergeRequest.source_branch], [ @@ -493,15 +491,11 @@ describe('IDE store merge request actions', () => { }); it('flashes message, if error', done => { - const flashSpy = spyOnDependency(actions, 'flash'); - store.dispatch.and.returnValue(Promise.reject()); + store.dispatch.mockRejectedValue(); openMergeRequest(store, mr) - .then(() => { - fail('Expected openMergeRequest to throw an error'); - }) .catch(() => { - expect(flashSpy).toHaveBeenCalledWith(jasmine.any(String)); + expect(createFlash).toHaveBeenCalledWith(expect.any(String)); }) .then(done) .catch(done.fail); diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js index e962224d1ad..1aaeebf19d4 100644 --- a/spec/javascripts/ide/stores/actions/project_spec.js +++ b/spec/frontend/ide/stores/actions/project_spec.js @@ -1,5 +1,7 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; +import { createStore } from '~/ide/stores'; +import router from '~/ide/ide_router'; import { refreshLastCommitData, showBranchNotFoundError, @@ -9,10 +11,8 @@ import { loadFile, loadBranch, } from '~/ide/stores/actions'; -import { createStore } from '~/ide/stores'; import service from '~/ide/services'; import api from '~/api'; -import router from '~/ide/ide_router'; import { resetStore } from '../../helpers'; import testAction from '../../../helpers/vuex_action_helper'; @@ -49,13 +49,11 @@ describe('IDE store project actions', () => { }, }, }; - spyOn(service, 'getBranchData').and.returnValue( - Promise.resolve({ - data: { - commit: { id: '123' }, - }, - }), - ); + jest.spyOn(service, 'getBranchData').mockResolvedValue({ + data: { + commit: { id: '123' }, + }, + }); }); it('calls the service', done => { @@ -110,7 +108,7 @@ describe('IDE store project actions', () => { type: 'setErrorMessage', payload: { text: "Branch <strong>master</strong> was not found in this project's repository.", - action: jasmine.any(Function), + action: expect.any(Function), actionText: 'Create branch', actionPayload: 'master', }, @@ -122,10 +120,12 @@ describe('IDE store project actions', () => { }); describe('createNewBranchFromDefault', () => { - it('calls API', done => { - spyOn(api, 'createBranch').and.returnValue(Promise.resolve()); - spyOn(router, 'push'); + beforeEach(() => { + jest.spyOn(api, 'createBranch').mockResolvedValue(); + jest.spyOn(router, 'push').mockImplementation(); + }); + it('calls API', done => { createNewBranchFromDefault( { state: { @@ -151,9 +151,7 @@ describe('IDE store project actions', () => { }); it('clears error message', done => { - const dispatchSpy = jasmine.createSpy('dispatch'); - spyOn(api, 'createBranch').and.returnValue(Promise.resolve()); - spyOn(router, 'push'); + const dispatchSpy = jest.fn().mockName('dispatch'); createNewBranchFromDefault( { @@ -177,9 +175,6 @@ describe('IDE store project actions', () => { }); it('reloads window', done => { - spyOn(api, 'createBranch').and.returnValue(Promise.resolve()); - spyOn(router, 'push'); - createNewBranchFromDefault( { state: { @@ -215,7 +210,7 @@ describe('IDE store project actions', () => { payload: { entry: store.state.trees[`${TEST_PROJECT_ID}/master`], forceValue: false }, }, ], - jasmine.any(Object), + expect.any(Object), done, ); }); @@ -243,7 +238,7 @@ describe('IDE store project actions', () => { 'foo/bar': { pending: false }, }, }); - spyOn(store, 'dispatch'); + jest.spyOn(store, 'dispatch').mockImplementation(); }); it('does nothing, if basePath is not given', () => { @@ -264,15 +259,15 @@ describe('IDE store project actions', () => { it('does not handle tree entry action, if entry is pending', () => { loadFile(store, { basePath: 'foo/bar-pending/' }); - expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything()); + 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.calls.count()).toBe(1); + expect(store.dispatch.mock.calls).toHaveLength(1); - expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', jasmine.anything()); + expect(store.dispatch).not.toHaveBeenCalledWith('handleTreeEntryAction', expect.anything()); expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', { name: 'not-existent.md', @@ -307,14 +302,14 @@ describe('IDE store project actions', () => { it('fetches branch data', done => { const mockGetters = { findBranch: () => ({ commit: { id: ref } }) }; - spyOn(store, 'dispatch').and.returnValue(Promise.resolve()); + jest.spyOn(store, 'dispatch').mockResolvedValue(); loadBranch( { getters: mockGetters, state: store.state, dispatch: store.dispatch }, { projectId, branchId }, ) .then(() => { - expect(store.dispatch.calls.allArgs()).toEqual([ + expect(store.dispatch.mock.calls).toEqual([ ['getBranchData', { projectId, branchId }], ['getMergeRequestsForBranch', { projectId, branchId }], ['getFiles', { projectId, branchId, ref }], @@ -325,12 +320,12 @@ describe('IDE store project actions', () => { }); it('shows an error if branch can not be fetched', done => { - spyOn(store, 'dispatch').and.returnValue(Promise.reject()); + jest.spyOn(store, 'dispatch').mockReturnValue(Promise.reject()); loadBranch(store, { projectId, branchId }) .then(done.fail) .catch(() => { - expect(store.dispatch.calls.allArgs()).toEqual([ + expect(store.dispatch.mock.calls).toEqual([ ['getBranchData', { projectId, branchId }], ['showBranchNotFoundError', branchId], ]); @@ -360,13 +355,13 @@ describe('IDE store project actions', () => { describe('existing branch', () => { beforeEach(() => { - spyOn(store, 'dispatch').and.returnValue(Promise.resolve()); + jest.spyOn(store, 'dispatch').mockResolvedValue(); }); it('dispatches branch actions', done => { openBranch(store, branch) .then(() => { - expect(store.dispatch.calls.allArgs()).toEqual([ + expect(store.dispatch.mock.calls).toEqual([ ['setCurrentBranchId', branchId], ['loadBranch', { projectId, branchId }], ['loadFile', { basePath: undefined }], @@ -379,13 +374,13 @@ describe('IDE store project actions', () => { describe('non-existent branch', () => { beforeEach(() => { - spyOn(store, 'dispatch').and.returnValue(Promise.reject()); + jest.spyOn(store, 'dispatch').mockReturnValue(Promise.reject()); }); it('dispatches correct branch actions', done => { openBranch(store, branch) .then(val => { - expect(store.dispatch.calls.allArgs()).toEqual([ + expect(store.dispatch.mock.calls).toEqual([ ['setCurrentBranchId', branchId], ['loadBranch', { projectId, branchId }], ]); diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js index 2201a3b4b57..37594512fe9 100644 --- a/spec/javascripts/ide/stores/actions/tree_spec.js +++ b/spec/frontend/ide/stores/actions/tree_spec.js @@ -1,5 +1,5 @@ import MockAdapter from 'axios-mock-adapter'; -import testAction from 'spec/helpers/vuex_action_helper'; +import testAction from 'helpers/vuex_action_helper'; import { showTreeEntry, getFiles, setDirectoryData } from '~/ide/stores/actions/tree'; import * as types from '~/ide/stores/mutation_types'; import axios from '~/lib/utils/axios_utils'; @@ -21,8 +21,7 @@ describe('Multi-file store tree actions', () => { }; beforeEach(() => { - jasmine.clock().install(); - spyOn(router, 'push'); + jest.spyOn(router, 'push').mockImplementation(); mock = new MockAdapter(axios); @@ -35,7 +34,6 @@ describe('Multi-file store tree actions', () => { }); afterEach(() => { - jasmine.clock().uninstall(); mock.restore(); resetStore(store); }); @@ -43,7 +41,7 @@ describe('Multi-file store tree actions', () => { describe('getFiles', () => { describe('success', () => { beforeEach(() => { - spyOn(service, 'getFiles').and.callThrough(); + jest.spyOn(service, 'getFiles'); mock .onGet(/(.*)/) @@ -54,15 +52,16 @@ describe('Multi-file store tree actions', () => { ]); }); - it('calls service getFiles', done => { - store - .dispatch('getFiles', basicCallParameters) - .then(() => { - expect(service.getFiles).toHaveBeenCalledWith('foo/abcproject', '12345678'); - - done(); - }) - .catch(done.fail); + it('calls service getFiles', () => { + return ( + store + .dispatch('getFiles', basicCallParameters) + // getFiles actions calls lodash.defer + .then(() => jest.runOnlyPendingTimers()) + .then(() => { + expect(service.getFiles).toHaveBeenCalledWith('foo/abcproject', '12345678'); + }) + ); }); it('adds data into tree', done => { @@ -71,7 +70,7 @@ describe('Multi-file store tree actions', () => { .then(() => { // The populating of the tree is deferred for performance reasons. // See this merge request for details: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/25700 - jasmine.clock().tick(1); + jest.advanceTimersByTime(1); }) .then(() => { projectTree = store.state.trees['abcproject/master']; @@ -91,7 +90,7 @@ describe('Multi-file store tree actions', () => { describe('error', () => { it('dispatches error action', done => { - const dispatch = jasmine.createSpy('dispatchSpy'); + const dispatch = jest.fn(); store.state.projects = { 'abc/def': { @@ -127,7 +126,7 @@ describe('Multi-file store tree actions', () => { .catch(() => { expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { text: 'An error occurred while loading all the files.', - action: jasmine.any(Function), + action: expect.any(Function), actionText: 'Please try again', actionPayload: { projectId: 'abc/def', branchId: 'master-testing' }, }); diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js index 364c8421b6b..d52b0435906 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/frontend/ide/stores/actions_spec.js @@ -1,5 +1,8 @@ import MockAdapter from 'axios-mock-adapter'; -import actions, { +import { visitUrl } from '~/lib/utils/url_utility'; +import { createStore } from '~/ide/stores'; +import router from '~/ide/ide_router'; +import { stageAllChanges, unstageAllChanges, toggleFileFinder, @@ -15,28 +18,29 @@ import actions, { discardAllChanges, } from '~/ide/stores/actions'; import axios from '~/lib/utils/axios_utils'; -import { createStore } from '~/ide/stores'; import * as types from '~/ide/stores/mutation_types'; -import router from '~/ide/ide_router'; import { file } from '../helpers'; import testAction from '../../helpers/vuex_action_helper'; import eventHub from '~/ide/eventhub'; +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), + joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths, +})); + describe('Multi-file store actions', () => { let store; beforeEach(() => { store = createStore(); - spyOn(store, 'commit').and.callThrough(); - spyOn(store, 'dispatch').and.callThrough(); - spyOn(router, 'push'); + jest.spyOn(store, 'commit'); + jest.spyOn(store, 'dispatch'); + jest.spyOn(router, 'push').mockImplementation(); }); describe('redirectToUrl', () => { it('calls visitUrl', done => { - const visitUrl = spyOnDependency(actions, 'visitUrl'); - store .dispatch('redirectToUrl', 'test') .then(() => { @@ -79,7 +83,7 @@ describe('Multi-file store actions', () => { discardAllChanges(store); - expect(store.dispatch.calls.allArgs()).toEqual(jasmine.arrayContaining(expectedCalls)); + expect(store.dispatch.mock.calls).toEqual(expect.arrayContaining(expectedCalls)); }); it('removes all files from changedFiles state', done => { @@ -255,7 +259,7 @@ describe('Multi-file store actions', () => { type: 'blob', }) .then(() => { - expect(store.state.stagedFiles).toEqual([jasmine.objectContaining({ name })]); + expect(store.state.stagedFiles).toEqual([expect.objectContaining({ name })]); done(); }) @@ -311,12 +315,12 @@ describe('Multi-file store actions', () => { document.body.innerHTML += '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>'; const el = document.querySelector('.repo-tab'); - spyOn(el, 'focus'); + jest.spyOn(el, 'focus').mockImplementation(); store .dispatch('scrollToTab') .then(() => { - setTimeout(() => { + setImmediate(() => { expect(el.focus).toHaveBeenCalled(); document.getElementById('tabs').remove(); @@ -350,16 +354,16 @@ describe('Multi-file store actions', () => { it('adds all files from changedFiles to stagedFiles', () => { stageAllChanges(store); - expect(store.commit.calls.allArgs()).toEqual([ + expect(store.commit.mock.calls).toEqual([ [types.SET_LAST_COMMIT_MSG, ''], - [types.STAGE_CHANGE, jasmine.objectContaining({ path: file1.path })], + [types.STAGE_CHANGE, expect.objectContaining({ path: file1.path })], ]); }); it('opens pending tab if a change exists in that file', () => { stageAllChanges(store); - expect(store.dispatch.calls.allArgs()).toEqual([ + expect(store.dispatch.mock.calls).toEqual([ [ 'openPendingTab', { file: { ...file1, staged: true, changed: true }, keyPrefix: 'staged' }, @@ -382,15 +386,15 @@ describe('Multi-file store actions', () => { it('removes all files from stagedFiles after unstaging', () => { unstageAllChanges(store); - expect(store.commit.calls.allArgs()).toEqual([ - [types.UNSTAGE_CHANGE, jasmine.objectContaining({ path: file2.path })], + expect(store.commit.mock.calls).toEqual([ + [types.UNSTAGE_CHANGE, expect.objectContaining({ path: file2.path })], ]); }); it('opens pending tab if a change exists in that file', () => { unstageAllChanges(store); - expect(store.dispatch.calls.allArgs()).toEqual([ + expect(store.dispatch.mock.calls).toEqual([ ['openPendingTab', { file: file1, keyPrefix: 'unstaged' }], ]); }); @@ -696,7 +700,7 @@ describe('Multi-file store actions', () => { describe('renameEntry', () => { describe('purging of file model cache', () => { beforeEach(() => { - spyOn(eventHub, '$emit'); + jest.spyOn(eventHub, '$emit').mockImplementation(); }); it('does not purge model cache for temporary entries that got renamed', done => { @@ -715,9 +719,7 @@ describe('Multi-file store actions', () => { name: 'new', }) .then(() => { - expect(eventHub.$emit.calls.allArgs()).not.toContain( - 'editor.update.model.dispose.foo-bar', - ); + expect(eventHub.$emit.mock.calls).not.toContain('editor.update.model.dispose.foo-bar'); }) .then(done) .catch(done.fail); @@ -768,17 +770,17 @@ describe('Multi-file store actions', () => { }); it('by default renames an entry and stages it', () => { - const dispatch = jasmine.createSpy(); - const commit = jasmine.createSpy(); + const dispatch = jest.fn(); + const commit = jest.fn(); renameEntry( { dispatch, commit, state: store.state, getters: store.getters }, { path: 'orig', name: 'renamed' }, ); - expect(commit.calls.allArgs()).toEqual([ + expect(commit.mock.calls).toEqual([ [types.RENAME_ENTRY, { path: 'orig', name: 'renamed', parentPath: undefined }], - [types.STAGE_CHANGE, jasmine.objectContaining({ path: 'renamed' })], + [types.STAGE_CHANGE, expect.objectContaining({ path: 'renamed' })], ]); }); @@ -813,7 +815,7 @@ describe('Multi-file store actions', () => { renameEntry, { path: 'orig', name: 'renamed' }, store.state, - [jasmine.objectContaining({ type: types.RENAME_ENTRY })], + [expect.objectContaining({ type: types.RENAME_ENTRY })], [{ type: 'triggerFilesChange' }], done, ); @@ -831,7 +833,7 @@ describe('Multi-file store actions', () => { name: 'renamed', }) .then(() => { - expect(router.push.calls.count()).toBe(1); + expect(router.push.mock.calls).toHaveLength(1); expect(router.push).toHaveBeenCalledWith(`/project/foo-bar.md`); }) .then(done) @@ -918,7 +920,7 @@ describe('Multi-file store actions', () => { expect(entries['new-folder']).toBeDefined(); expect(entries['new-folder/test']).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ path: 'new-folder/test', name: 'test', prevPath: 'old-folder/test', @@ -941,7 +943,7 @@ describe('Multi-file store actions', () => { expect(entries['old-folder']).toBeDefined(); expect(entries['old-folder/test']).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ path: 'old-folder/test', name: 'test', prevPath: undefined, @@ -989,10 +991,10 @@ describe('Multi-file store actions', () => { .dispatch('renameEntry', { path: filePath, name: fileName, parentPath: newParentPath }) .then(() => { expect(store.state.entries[newParentPath]).toEqual( - jasmine.objectContaining({ + expect.objectContaining({ path: newParentPath, type: 'tree', - tree: jasmine.arrayContaining([ + tree: expect.arrayContaining([ store.state.entries[`${newParentPath}/${fileName}`], ]), }), @@ -1078,7 +1080,7 @@ describe('Multi-file store actions', () => { branchId: 'master-testing', }, ]; - dispatch = jasmine.createSpy('dispatchSpy'); + dispatch = jest.fn(); document.body.innerHTML += '<div class="flash-container"></div>'; }); @@ -1092,7 +1094,7 @@ describe('Multi-file store actions', () => { getBranchData(...callParams) .then(done.fail) .catch(e => { - expect(dispatch.calls.count()).toEqual(0); + expect(dispatch.mock.calls).toHaveLength(0); expect(e.response.status).toEqual(404); expect(document.querySelector('.flash-alert')).toBeNull(); done(); @@ -1105,7 +1107,7 @@ describe('Multi-file store actions', () => { getBranchData(...callParams) .then(done.fail) .catch(e => { - expect(dispatch.calls.count()).toEqual(0); + expect(dispatch.mock.calls).toHaveLength(0); expect(e.response).toBeUndefined(); expect(document.querySelector('.flash-alert')).not.toBeNull(); done(); diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js index fb8cb300209..649c05441f6 100644 --- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js +++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js @@ -1,5 +1,7 @@ -import { resetStore, file } from 'spec/ide/helpers'; -import rootActions from '~/ide/stores/actions'; +import { resetStore, file } from 'jest/ide/helpers'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { visitUrl } from '~/lib/utils/url_utility'; import { createStore } from '~/ide/stores'; import service from '~/ide/services'; import router from '~/ide/ide_router'; @@ -10,15 +12,28 @@ import * as actions from '~/ide/stores/modules/commit/actions'; import { commitActionTypes, PERMISSION_CREATE_MR } from '~/ide/constants'; import testAction from '../../../../helpers/vuex_action_helper'; +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), + joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths, +})); + const TEST_COMMIT_SHA = '123456789'; const store = createStore(); describe('IDE commit module actions', () => { + let mock; + beforeEach(() => { - spyOn(router, 'push'); + gon.api_version = 'v1'; + mock = new MockAdapter(axios); + jest.spyOn(router, 'push').mockImplementation(); + + mock.onGet('/api/v1/projects/abcproject/repository/branches/master').reply(200); }); afterEach(() => { + delete gon.api_version; + mock.restore(); resetStore(store); }); @@ -71,7 +86,7 @@ describe('IDE commit module actions', () => { [ { type: mutationTypes.UPDATE_COMMIT_ACTION, - payload: { commitAction: jasmine.anything() }, + payload: { commitAction: expect.anything() }, }, { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: true }, ], @@ -92,7 +107,7 @@ describe('IDE commit module actions', () => { [ { type: mutationTypes.UPDATE_COMMIT_ACTION, - payload: { commitAction: jasmine.anything() }, + payload: { commitAction: expect.anything() }, }, { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR, payload: false }, ], @@ -168,7 +183,7 @@ describe('IDE commit module actions', () => { let f; beforeEach(() => { - spyOn(eventHub, '$emit'); + jest.spyOn(eventHub, '$emit').mockImplementation(); f = file('changedFile'); Object.assign(f, { @@ -200,9 +215,9 @@ describe('IDE commit module actions', () => { changed: true, }, ], - openFiles: store.state.stagedFiles, }); + store.state.openFiles = store.state.stagedFiles; store.state.stagedFiles.forEach(stagedFile => { store.state.entries[stagedFile.path] = stagedFile; }); @@ -280,11 +295,7 @@ describe('IDE commit module actions', () => { }); describe('commitChanges', () => { - let visitUrl; - beforeEach(() => { - visitUrl = spyOnDependency(rootActions, 'visitUrl'); - document.body.innerHTML += '<div class="flash-container"></div>'; const f = { @@ -346,11 +357,7 @@ describe('IDE commit module actions', () => { }; beforeEach(() => { - spyOn(service, 'commit').and.returnValue( - Promise.resolve({ - data: COMMIT_RESPONSE, - }), - ); + jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE }); }); it('calls service', done => { @@ -358,14 +365,14 @@ describe('IDE commit module actions', () => { .dispatch('commit/commitChanges') .then(() => { expect(service.commit).toHaveBeenCalledWith('abcproject', { - branch: jasmine.anything(), + branch: expect.anything(), commit_message: 'testing 123', actions: [ { action: commitActionTypes.update, - file_path: jasmine.anything(), + file_path: expect.anything(), content: '\n', - encoding: jasmine.anything(), + encoding: expect.anything(), last_commit_id: undefined, previous_path: undefined, }, @@ -385,14 +392,14 @@ describe('IDE commit module actions', () => { .dispatch('commit/commitChanges') .then(() => { expect(service.commit).toHaveBeenCalledWith('abcproject', { - branch: jasmine.anything(), + branch: expect.anything(), commit_message: 'testing 123', actions: [ { action: commitActionTypes.update, - file_path: jasmine.anything(), + file_path: expect.anything(), content: '\n', - encoding: jasmine.anything(), + encoding: expect.anything(), last_commit_id: TEST_COMMIT_SHA, previous_path: undefined, }, @@ -455,7 +462,7 @@ describe('IDE commit module actions', () => { describe('merge request', () => { it('redirects to new merge request page', done => { - spyOn(eventHub, '$on'); + jest.spyOn(eventHub, '$on').mockImplementation(); store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; store.state.commit.shouldCreateMR = true; @@ -475,7 +482,7 @@ describe('IDE commit module actions', () => { }); it('does not redirect to new merge request page when shouldCreateMR is not checked', done => { - spyOn(eventHub, '$on'); + jest.spyOn(eventHub, '$on').mockImplementation(); store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH; store.state.commit.shouldCreateMR = false; @@ -489,30 +496,25 @@ describe('IDE commit module actions', () => { .catch(done.fail); }); - it('resets changed files before redirecting', done => { - visitUrl = visitUrl.and.callFake(() => { - expect(store.state.stagedFiles.length).toBe(0); - done(); - }); - - spyOn(eventHub, '$on'); + it('resets changed files before redirecting', () => { + jest.spyOn(eventHub, '$on').mockImplementation(); store.state.commit.commitAction = '3'; - store.dispatch('commit/commitChanges').catch(done.fail); + return store.dispatch('commit/commitChanges').then(() => { + expect(store.state.stagedFiles.length).toBe(0); + }); }); }); }); describe('failed', () => { beforeEach(() => { - spyOn(service, 'commit').and.returnValue( - Promise.resolve({ - data: { - message: 'failed message', - }, - }), - ); + jest.spyOn(service, 'commit').mockResolvedValue({ + data: { + message: 'failed message', + }, + }); }); it('shows failed message', done => { @@ -543,20 +545,15 @@ describe('IDE commit module actions', () => { }; it('commits TOGGLE_EMPTY_STATE mutation on empty repo', done => { - spyOn(service, 'commit').and.returnValue( - Promise.resolve({ - data: COMMIT_RESPONSE, - }), - ); - - spyOn(store, 'commit').and.callThrough(); + jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE }); + jest.spyOn(store, 'commit'); store .dispatch('commit/commitChanges') .then(() => { - expect(store.commit.calls.allArgs()).toEqual( - jasmine.arrayContaining([ - ['TOGGLE_EMPTY_STATE', jasmine.any(Object), jasmine.any(Object)], + expect(store.commit.mock.calls).toEqual( + expect.arrayContaining([ + ['TOGGLE_EMPTY_STATE', expect.any(Object), expect.any(Object)], ]), ); done(); @@ -566,19 +563,15 @@ describe('IDE commit module actions', () => { it('does not commmit TOGGLE_EMPTY_STATE mutation on existing project', done => { COMMIT_RESPONSE.parent_ids.push('1234'); - spyOn(service, 'commit').and.returnValue( - Promise.resolve({ - data: COMMIT_RESPONSE, - }), - ); - spyOn(store, 'commit').and.callThrough(); + jest.spyOn(service, 'commit').mockResolvedValue({ data: COMMIT_RESPONSE }); + jest.spyOn(store, 'commit'); store .dispatch('commit/commitChanges') .then(() => { - expect(store.commit.calls.allArgs()).not.toEqual( - jasmine.arrayContaining([ - ['TOGGLE_EMPTY_STATE', jasmine.any(Object), jasmine.any(Object)], + expect(store.commit.mock.calls).not.toEqual( + expect.arrayContaining([ + ['TOGGLE_EMPTY_STATE', expect.any(Object), expect.any(Object)], ]), ); done(); diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js index ea975500e8d..f689314567a 100644 --- a/spec/frontend/ide/utils_spec.js +++ b/spec/frontend/ide/utils_spec.js @@ -1,6 +1,4 @@ -import { commitItemIconMap } from '~/ide/constants'; -import { getCommitIconMap, isTextFile, registerLanguages, trimPathComponents } from '~/ide/utils'; -import { decorateData } from '~/ide/stores/utils'; +import { isTextFile, registerLanguages, trimPathComponents } from '~/ide/utils'; import { languages } from 'monaco-editor'; describe('WebIDE utils', () => { @@ -62,48 +60,6 @@ describe('WebIDE utils', () => { }); }); - const createFile = (name = 'name', id = name, type = '', parent = null) => - decorateData({ - id, - type, - icon: 'icon', - url: 'url', - name, - path: parent ? `${parent.path}/${name}` : name, - parentPath: parent ? parent.path : '', - lastCommit: {}, - }); - - describe('getCommitIconMap', () => { - let entry; - - beforeEach(() => { - entry = createFile('Entry item'); - }); - - it('renders "deleted" icon for deleted entries', () => { - entry.deleted = true; - expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.deleted); - }); - - it('renders "addition" icon for temp entries', () => { - entry.tempFile = true; - expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.addition); - }); - - it('renders "modified" icon for newly-renamed entries', () => { - entry.prevPath = 'foo/bar'; - entry.tempFile = false; - expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified); - }); - - it('renders "modified" icon even for temp entries if they are newly-renamed', () => { - entry.prevPath = 'foo/bar'; - entry.tempFile = true; - expect(getCommitIconMap(entry)).toEqual(commitItemIconMap.modified); - }); - }); - describe('trimPathComponents', () => { it.each` input | output diff --git a/spec/frontend/notes/components/diff_with_note_spec.js b/spec/frontend/notes/components/diff_with_note_spec.js index d6d42e1988d..6480af015db 100644 --- a/spec/frontend/notes/components/diff_with_note_spec.js +++ b/spec/frontend/notes/components/diff_with_note_spec.js @@ -1,4 +1,4 @@ -import { mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import DiffWithNote from '~/notes/components/diff_with_note.vue'; import { createStore } from '~/mr_notes/stores'; @@ -37,7 +37,7 @@ describe('diff_with_note', () => { beforeEach(() => { const diffDiscussion = getJSONFixture(discussionFixture)[0]; - wrapper = mount(DiffWithNote, { + wrapper = shallowMount(DiffWithNote, { propsData: { discussion: diffDiscussion, }, @@ -76,7 +76,10 @@ describe('diff_with_note', () => { describe('image diff', () => { beforeEach(() => { const imageDiscussion = getJSONFixture(imageDiscussionFixture)[0]; - wrapper = mount(DiffWithNote, { propsData: { discussion: imageDiscussion }, store }); + wrapper = shallowMount(DiffWithNote, { + propsData: { discussion: imageDiscussion, diffFile: {} }, + store, + }); }); it('shows image diff', () => { diff --git a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js index 636508be6b6..a6e4d812c3c 100644 --- a/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js +++ b/spec/frontend/vue_shared/components/diff_viewer/diff_viewer_spec.js @@ -8,6 +8,7 @@ describe('DiffViewer', () => { const requiredProps = { diffMode: 'replaced', diffViewerMode: 'image', + diffFile: {}, newPath: GREEN_BOX_IMAGE_URL, newSha: 'ABC', oldPath: RED_BOX_IMAGE_URL, @@ -71,16 +72,27 @@ describe('DiffViewer', () => { }); }); - it('renders renamed component', () => { - createComponent({ - ...requiredProps, - diffMode: 'renamed', - diffViewerMode: 'renamed', - newPath: 'test.abc', - oldPath: 'testold.abc', + describe('renamed file', () => { + it.each` + altViewer + ${'text'} + ${'notText'} + `('renders the renamed component when the alternate viewer is $altViewer', ({ altViewer }) => { + createComponent({ + ...requiredProps, + diffFile: { + content_sha: '', + view_path: '', + alternate_viewer: { name: altViewer }, + }, + diffMode: 'renamed', + diffViewerMode: 'renamed', + newPath: 'test.abc', + oldPath: 'testold.abc', + }); + + expect(vm.$el.textContent).toContain('File renamed with no changes.'); }); - - expect(vm.$el.textContent).toContain('File moved'); }); it('renders mode changed component', () => { diff --git a/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js new file mode 100644 index 00000000000..13584d7aeeb --- /dev/null +++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/renamed_spec.js @@ -0,0 +1,283 @@ +import Vuex from 'vuex'; +import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; +import Renamed from '~/vue_shared/components/diff_viewer/viewers/renamed.vue'; +import { + TRANSITION_LOAD_START, + TRANSITION_LOAD_ERROR, + TRANSITION_LOAD_SUCCEED, + TRANSITION_ACKNOWLEDGE_ERROR, + STATE_IDLING, + STATE_LOADING, + STATE_ERRORED, +} from '~/diffs/constants'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +function createRenamedComponent({ + props = {}, + methods = {}, + store = new Vuex.Store({}), + deep = false, +}) { + const mnt = deep ? mount : shallowMount; + + return mnt(Renamed, { + propsData: { ...props }, + localVue, + store, + methods, + }); +} + +describe('Renamed Diff Viewer', () => { + const DIFF_FILE_COMMIT_SHA = 'commitsha'; + const DIFF_FILE_SHORT_SHA = 'commitsh'; + const DIFF_FILE_VIEW_PATH = `blob/${DIFF_FILE_COMMIT_SHA}/filename.ext`; + let diffFile; + let wrapper; + + beforeEach(() => { + diffFile = { + content_sha: DIFF_FILE_COMMIT_SHA, + view_path: DIFF_FILE_VIEW_PATH, + alternate_viewer: { + name: 'text', + }, + }; + }); + + afterEach(() => { + if (wrapper) { + wrapper.destroy(); + wrapper = null; + } + }); + + describe('is', () => { + beforeEach(() => { + wrapper = createRenamedComponent({ props: { diffFile } }); + }); + + it.each` + state | request | result + ${'idle'} | ${'idle'} | ${true} + ${'idle'} | ${'loading'} | ${false} + ${'idle'} | ${'errored'} | ${false} + ${'loading'} | ${'loading'} | ${true} + ${'loading'} | ${'idle'} | ${false} + ${'loading'} | ${'errored'} | ${false} + ${'errored'} | ${'errored'} | ${true} + ${'errored'} | ${'idle'} | ${false} + ${'errored'} | ${'loading'} | ${false} + `( + 'returns the $result for "$request" when the state is "$state"', + ({ request, result, state }) => { + wrapper.vm.state = state; + + expect(wrapper.vm.is(request)).toEqual(result); + }, + ); + }); + + describe('transition', () => { + beforeEach(() => { + wrapper = createRenamedComponent({ props: { diffFile } }); + }); + + it.each` + state | transition | result + ${'idle'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING} + ${'idle'} | ${TRANSITION_LOAD_ERROR} | ${STATE_IDLING} + ${'idle'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_IDLING} + ${'idle'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLING} + ${'loading'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING} + ${'loading'} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED} + ${'loading'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_IDLING} + ${'loading'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_LOADING} + ${'errored'} | ${TRANSITION_LOAD_START} | ${STATE_LOADING} + ${'errored'} | ${TRANSITION_LOAD_ERROR} | ${STATE_ERRORED} + ${'errored'} | ${TRANSITION_LOAD_SUCCEED} | ${STATE_ERRORED} + ${'errored'} | ${TRANSITION_ACKNOWLEDGE_ERROR} | ${STATE_IDLING} + `( + 'correctly updates the state to "$result" when it starts as "$state" and the transition is "$transition"', + ({ state, transition, result }) => { + wrapper.vm.state = state; + + wrapper.vm.transition(transition); + + expect(wrapper.vm.state).toEqual(result); + }, + ); + }); + + describe('switchToFull', () => { + let store; + + beforeEach(() => { + store = new Vuex.Store({ + modules: { + diffs: { + namespaced: true, + actions: { switchToFullDiffFromRenamedFile: () => {} }, + }, + }, + }); + + jest.spyOn(store, 'dispatch'); + + wrapper = createRenamedComponent({ props: { diffFile }, store }); + }); + + afterEach(() => { + store = null; + }); + + it('calls the switchToFullDiffFromRenamedFile action when the method is triggered', () => { + store.dispatch.mockResolvedValue(); + + wrapper.vm.switchToFull(); + + return wrapper.vm.$nextTick().then(() => { + expect(store.dispatch).toHaveBeenCalledWith('diffs/switchToFullDiffFromRenamedFile', { + diffFile, + }); + }); + }); + + it.each` + after | resolvePromise | resolution + ${STATE_IDLING} | ${'mockResolvedValue'} | ${'successful'} + ${STATE_ERRORED} | ${'mockRejectedValue'} | ${'rejected'} + `( + 'moves through the correct states during a $resolution request', + ({ after, resolvePromise }) => { + store.dispatch[resolvePromise](); + + expect(wrapper.vm.state).toEqual(STATE_IDLING); + + wrapper.vm.switchToFull(); + + expect(wrapper.vm.state).toEqual(STATE_LOADING); + + return ( + wrapper.vm + // This tick is needed for when the action (promise) finishes + .$nextTick() + // This tick waits for the state change in the promise .then/.catch to bubble into the component + .then(() => wrapper.vm.$nextTick()) + .then(() => { + expect(wrapper.vm.state).toEqual(after); + }) + ); + }, + ); + }); + + describe('clickLink', () => { + let event; + + beforeEach(() => { + event = { + preventDefault: jest.fn(), + }; + }); + + it.each` + alternateViewer | stops | handled + ${'text'} | ${true} | ${'should'} + ${'nottext'} | ${false} | ${'should not'} + `( + 'given { alternate_viewer: { name: "$alternateViewer" } }, the click event $handled be handled in the component', + ({ alternateViewer, stops }) => { + wrapper = createRenamedComponent({ + props: { + diffFile: { + ...diffFile, + alternate_viewer: { name: alternateViewer }, + }, + }, + }); + + jest.spyOn(wrapper.vm, 'switchToFull').mockImplementation(() => {}); + + wrapper.vm.clickLink(event); + + if (stops) { + expect(event.preventDefault).toHaveBeenCalled(); + expect(wrapper.vm.switchToFull).toHaveBeenCalled(); + } else { + expect(event.preventDefault).not.toHaveBeenCalled(); + expect(wrapper.vm.switchToFull).not.toHaveBeenCalled(); + } + }, + ); + }); + + describe('dismissError', () => { + let transitionSpy; + + beforeEach(() => { + wrapper = createRenamedComponent({ props: { diffFile } }); + transitionSpy = jest.spyOn(wrapper.vm, 'transition'); + }); + + it(`transitions the component with "${TRANSITION_ACKNOWLEDGE_ERROR}"`, () => { + wrapper.vm.dismissError(); + + expect(transitionSpy).toHaveBeenCalledWith(TRANSITION_ACKNOWLEDGE_ERROR); + }); + }); + + describe('output', () => { + it.each` + altViewer | nameDisplay + ${'text'} | ${'"text"'} + ${'nottext'} | ${'"nottext"'} + ${undefined} | ${undefined} + ${null} | ${null} + `( + 'with { alternate_viewer: { name: $nameDisplay } }, renders the component', + ({ altViewer }) => { + const file = { ...diffFile }; + + file.alternate_viewer.name = altViewer; + wrapper = createRenamedComponent({ props: { diffFile: file } }); + + expect(wrapper.find('[test-id="plaintext"]').text()).toEqual( + 'File renamed with no changes.', + ); + }, + ); + + it.each` + altType | linkText + ${'text'} | ${'Show file contents'} + ${'nottext'} | ${`View file @ ${DIFF_FILE_SHORT_SHA}`} + `( + 'includes a link to the full file for alternate viewer type "$altType"', + ({ altType, linkText }) => { + const file = { ...diffFile }; + const clickMock = jest.fn().mockImplementation(() => {}); + + file.alternate_viewer.name = altType; + wrapper = createRenamedComponent({ + deep: true, + props: { diffFile: file }, + methods: { + clickLink: clickMock, + }, + }); + + const link = wrapper.find('a'); + + expect(link.text()).toEqual(linkText); + expect(link.attributes('href')).toEqual(DIFF_FILE_VIEW_PATH); + + link.trigger('click'); + + expect(clickMock).toHaveBeenCalled(); + }, + ); + }); +}); diff --git a/spec/javascripts/ide/components/jobs/detail_spec.js b/spec/javascripts/ide/components/jobs/detail_spec.js deleted file mode 100644 index a4e6b81acba..00000000000 --- a/spec/javascripts/ide/components/jobs/detail_spec.js +++ /dev/null @@ -1,184 +0,0 @@ -import Vue from 'vue'; -import JobDetail from '~/ide/components/jobs/detail.vue'; -import { createStore } from '~/ide/stores'; -import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; -import { jobs } from '../../mock_data'; - -describe('IDE jobs detail view', () => { - const Component = Vue.extend(JobDetail); - let vm; - - beforeEach(() => { - const store = createStore(); - - store.state.pipelines.detailJob = { - ...jobs[0], - isLoading: true, - output: 'testing', - rawPath: `${gl.TEST_HOST}/raw`, - }; - - vm = createComponentWithStore(Component, store); - - spyOn(vm, 'fetchJobTrace').and.returnValue(Promise.resolve()); - - vm = vm.$mount(); - - spyOn(vm.$refs.buildTrace, 'scrollTo'); - }); - - afterEach(() => { - vm.$destroy(); - }); - - it('calls fetchJobTrace on mount', () => { - expect(vm.fetchJobTrace).toHaveBeenCalled(); - }); - - it('scrolls to bottom on mount', done => { - setTimeout(() => { - expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalled(); - - done(); - }); - }); - - it('renders job output', () => { - expect(vm.$el.querySelector('.bash').textContent).toContain('testing'); - }); - - it('renders empty message output', done => { - vm.$store.state.pipelines.detailJob.output = ''; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.bash').textContent).toContain('No messages were logged'); - - done(); - }); - }); - - it('renders loading icon', () => { - expect(vm.$el.querySelector('.build-loader-animation')).not.toBe(null); - expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe(''); - }); - - it('hides output when loading', () => { - expect(vm.$el.querySelector('.bash')).not.toBe(null); - expect(vm.$el.querySelector('.bash').style.display).toBe('none'); - }); - - it('hide loading icon when isLoading is false', done => { - vm.$store.state.pipelines.detailJob.isLoading = false; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.build-loader-animation').style.display).toBe('none'); - - done(); - }); - }); - - it('resets detailJob when clicking header button', () => { - spyOn(vm, 'setDetailJob'); - - vm.$el.querySelector('.btn').click(); - - expect(vm.setDetailJob).toHaveBeenCalledWith(null); - }); - - it('renders raw path link', () => { - expect(vm.$el.querySelector('.controllers-buttons').getAttribute('href')).toBe( - `${gl.TEST_HOST}/raw`, - ); - }); - - describe('scroll buttons', () => { - it('triggers scrollDown when clicking down button', done => { - spyOn(vm, 'scrollDown'); - - vm.$el.querySelectorAll('.btn-scroll')[1].click(); - - vm.$nextTick(() => { - expect(vm.scrollDown).toHaveBeenCalled(); - - done(); - }); - }); - - it('triggers scrollUp when clicking up button', done => { - spyOn(vm, 'scrollUp'); - - vm.scrollPos = 1; - - vm.$nextTick() - .then(() => vm.$el.querySelector('.btn-scroll').click()) - .then(() => vm.$nextTick()) - .then(() => { - expect(vm.scrollUp).toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('scrollDown', () => { - it('scrolls build trace to bottom', () => { - spyOnProperty(vm.$refs.buildTrace, 'scrollHeight').and.returnValue(1000); - - vm.scrollDown(); - - expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 1000); - }); - }); - - describe('scrollUp', () => { - it('scrolls build trace to top', () => { - vm.scrollUp(); - - expect(vm.$refs.buildTrace.scrollTo).toHaveBeenCalledWith(0, 0); - }); - }); - - describe('scrollBuildLog', () => { - beforeEach(() => { - spyOnProperty(vm.$refs.buildTrace, 'offsetHeight').and.returnValue(100); - spyOnProperty(vm.$refs.buildTrace, 'scrollHeight').and.returnValue(200); - }); - - it('sets scrollPos to bottom when at the bottom', done => { - spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(100); - - vm.scrollBuildLog(); - - setTimeout(() => { - expect(vm.scrollPos).toBe(1); - - done(); - }); - }); - - it('sets scrollPos to top when at the top', done => { - spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(0); - vm.scrollPos = 1; - - vm.scrollBuildLog(); - - setTimeout(() => { - expect(vm.scrollPos).toBe(0); - - done(); - }); - }); - - it('resets scrollPos when not at top or bottom', done => { - spyOnProperty(vm.$refs.buildTrace, 'scrollTop').and.returnValue(10); - - vm.scrollBuildLog(); - - setTimeout(() => { - expect(vm.scrollPos).toBe(''); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js deleted file mode 100644 index 2c52780f316..00000000000 --- a/spec/javascripts/ide/helpers.js +++ /dev/null @@ -1 +0,0 @@ -export * from '../../frontend/ide/helpers'; diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js deleted file mode 100644 index 27f0ad01f54..00000000000 --- a/spec/javascripts/ide/mock_data.js +++ /dev/null @@ -1 +0,0 @@ -export * from '../../frontend/ide/mock_data'; diff --git a/spec/javascripts/vue_shared/components/file_finder/index_spec.js b/spec/javascripts/vue_shared/components/file_finder/index_spec.js index 7ded228d3ea..bb927b7d7f2 100644 --- a/spec/javascripts/vue_shared/components/file_finder/index_spec.js +++ b/spec/javascripts/vue_shared/components/file_finder/index_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import Mousetrap from 'mousetrap'; -import { file } from 'spec/ide/helpers'; +import { file } from 'jest/ide/helpers'; import timeoutPromise from 'spec/helpers/set_timeout_promise_helper'; import FindFileComponent from '~/vue_shared/components/file_finder/index.vue'; import { UP_KEY_CODE, DOWN_KEY_CODE, ENTER_KEY_CODE, ESC_KEY_CODE } from '~/lib/utils/keycodes'; diff --git a/spec/models/project_services/emails_on_push_service_spec.rb b/spec/models/project_services/emails_on_push_service_spec.rb index ce1952b503f..44db95afc57 100644 --- a/spec/models/project_services/emails_on_push_service_spec.rb +++ b/spec/models/project_services/emails_on_push_service_spec.rb @@ -21,23 +21,25 @@ describe EmailsOnPushService do end end - context 'when properties is missing branches_to_be_notified' do - subject { described_class.new(properties: {}) } + describe '.new' do + context 'when properties is missing branches_to_be_notified' do + subject { described_class.new(properties: {}) } - it 'sets the default value to all' do - expect(subject.branches_to_be_notified).to eq('all') + it 'sets the default value to all' do + expect(subject.branches_to_be_notified).to eq('all') + end end - end - context 'when branches_to_be_notified is already set' do - subject { described_class.new(properties: { branches_to_be_notified: 'protected' }) } + context 'when branches_to_be_notified is already set' do + subject { described_class.new(properties: { branches_to_be_notified: 'protected' }) } - it 'does not overwrite it with the default value' do - expect(subject.branches_to_be_notified).to eq('protected') + it 'does not overwrite it with the default value' do + expect(subject.branches_to_be_notified).to eq('protected') + end end end - context 'project emails' do + describe '#execute' do let(:push_data) { { object_kind: 'push' } } let(:project) { create(:project, :repository) } let(:service) { create(:emails_on_push_service, project: project) } |