diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index c1cab844ca6..53cc0a57ee4 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -202,7 +202,7 @@ - name: redis:alpine .use-pg10: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.12-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" services: - name: postgres:10.9 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] @@ -216,7 +216,7 @@ - name: docker.elastic.co/elasticsearch/elasticsearch:5.6.12 .use-pg10-ee: - image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.11-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" + image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.3-golang-1.12-git-2.22-chrome-73.0-node-12.x-yarn-1.16-postgresql-10-graphicsmagick-1.3.33" services: - name: postgres:10.9 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js deleted file mode 100644 index 03369febb4a..00000000000 --- a/app/assets/javascripts/boards/services/board_service.js +++ /dev/null @@ -1,98 +0,0 @@ -/* eslint-disable class-methods-use-this */ -/** - * This file is intended to be deleted. - * The existing functions will removed one by one in favor of using the board store directly. - * see https://gitlab.com/gitlab-org/gitlab-foss/issues/61621 - */ - -import boardsStore from '~/boards/stores/boards_store'; - -export default class BoardService { - generateBoardsPath(id) { - return boardsStore.generateBoardsPath(id); - } - - generateIssuesPath(id) { - return boardsStore.generateIssuesPath(id); - } - - static generateIssuePath(boardId, id) { - return boardsStore.generateIssuePath(boardId, id); - } - - all() { - return boardsStore.all(); - } - - generateDefaultLists() { - return boardsStore.generateDefaultLists(); - } - - createList(entityId, entityType) { - return boardsStore.createList(entityId, entityType); - } - - updateList(id, position, collapsed) { - return boardsStore.updateList(id, position, collapsed); - } - - destroyList(id) { - return boardsStore.destroyList(id); - } - - getIssuesForList(id, filter = {}) { - return boardsStore.getIssuesForList(id, filter); - } - - moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) { - return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId); - } - - moveMultipleIssues({ - ids, - fromListId = null, - toListId = null, - moveBeforeId = null, - moveAfterId = null, - }) { - return boardsStore.moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId }); - } - - newIssue(id, issue) { - return boardsStore.newIssue(id, issue); - } - - getBacklog(data) { - return boardsStore.getBacklog(data); - } - - bulkUpdate(issueIds, extraData = {}) { - return boardsStore.bulkUpdate(issueIds, extraData); - } - - static getIssueInfo(endpoint) { - return boardsStore.getIssueInfo(endpoint); - } - - static toggleIssueSubscription(endpoint) { - return boardsStore.toggleIssueSubscription(endpoint); - } - - allBoards() { - return boardsStore.allBoards(); - } - - recentBoards() { - return boardsStore.recentBoards(); - } - - createBoard(board) { - return boardsStore.createBoard(board); - } - - deleteBoard({ id }) { - return boardsStore.deleteBoard({ id }); - } -} - -window.BoardService = BoardService; diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 9d1de4ef8a0..7df99610132 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -1,6 +1,6 @@ + + diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 363fe226f15..31dbddbd21a 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -7,6 +7,7 @@ import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_ import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service'; import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps'; import createFlash from '../flash'; +import Loading from './components/loading.vue'; import WidgetHeader from './components/mr_widget_header.vue'; import WidgetMergeHelp from './components/mr_widget_merge_help.vue'; import MrWidgetPipelineContainer from './components/mr_widget_pipeline_container.vue'; @@ -44,6 +45,7 @@ export default { // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings name: 'MRWidget', components: { + Loading, 'mr-widget-header': WidgetHeader, 'mr-widget-merge-help': WidgetMergeHelp, MrWidgetPipelineContainer, @@ -80,12 +82,12 @@ export default { }, }, data() { - const store = new MRWidgetStore(this.mrData || window.gl.mrWidgetData); - const service = this.createService(store); + const store = this.mrData && new MRWidgetStore(this.mrData); + return { mr: store, - state: store.state, - service, + state: store && store.state, + service: store && this.createService(store), }; }, computed: { @@ -133,29 +135,58 @@ export default { } }, }, - created() { - this.initPolling(); - this.bindEventHubListeners(); - eventHub.$on('mr.discussion.updated', this.checkStatus); - }, mounted() { - this.setFaviconHelper(); - this.initDeploymentsPolling(); - - if (this.shouldRenderMergedPipeline) { - this.initPostMergeDeploymentsPolling(); + if (gon && gon.features && gon.features.asyncMrWidget) { + MRWidgetService.fetchInitialData() + .then(({ data }) => this.initWidget(data)) + .catch(() => + createFlash(__('Unable to load the merge request widget. Try reloading the page.')), + ); + } else { + this.initWidget(); } }, beforeDestroy() { eventHub.$off('mr.discussion.updated', this.checkStatus); - this.pollingInterval.destroy(); - this.deploymentsInterval.destroy(); + if (this.pollingInterval) { + this.pollingInterval.destroy(); + } + + if (this.deploymentsInterval) { + this.deploymentsInterval.destroy(); + } if (this.postMergeDeploymentsInterval) { this.postMergeDeploymentsInterval.destroy(); } }, methods: { + initWidget(data = {}) { + if (this.mr) { + this.mr.setData({ ...window.gl.mrWidgetData, ...data }); + } else { + this.mr = new MRWidgetStore({ ...window.gl.mrWidgetData, ...data }); + } + + if (!this.state) { + this.state = this.mr.state; + } + + if (!this.service) { + this.service = this.createService(this.mr); + } + + this.setFaviconHelper(); + this.initDeploymentsPolling(); + + if (this.shouldRenderMergedPipeline) { + this.initPostMergeDeploymentsPolling(); + } + + this.initPolling(); + this.bindEventHubListeners(); + eventHub.$on('mr.discussion.updated', this.checkStatus); + }, getServiceEndpoints(store) { return { mergePath: store.mergePath, @@ -319,7 +350,7 @@ export default { }; diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js index 8a229d80954..d22cb4ced80 100644 --- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js +++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js @@ -61,4 +61,11 @@ export default class MRWidgetService { static fetchMetrics(metricsUrl) { return axios.get(`${metricsUrl}.json`); } + + static fetchInitialData() { + return Promise.all([ + axios.get(window.gl.mrWidgetData.merge_request_cached_widget_path), + axios.get(window.gl.mrWidgetData.merge_request_widget_path), + ]).then(axios.spread((res, cachedRes) => ({ data: Object.assign(res.data, cachedRes.data) }))); + } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index eb7d162e38c..c023c9e5cbd 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -51,6 +51,10 @@ position: relative; border: 1px solid $border-color; border-radius: $border-radius-default; + + .gl-skeleton-loader { + display: block; + } } .mr-widget-extension { diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 844f1d04679..07f568e2a04 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -25,6 +25,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action do push_frontend_feature_flag(:vue_issuable_sidebar, @project.group) push_frontend_feature_flag(:release_search_filter, @project, default_enabled: true) + push_frontend_feature_flag(:async_mr_widget, @project) end around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions] diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 4d35353d5f5..e3ef8f3f2ff 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -11,7 +11,6 @@ class Projects::PipelinesController < Projects::ApplicationController before_action :authorize_create_pipeline!, only: [:new, :create] before_action :authorize_update_pipeline!, only: [:retry, :cancel] before_action do - push_frontend_feature_flag(:hide_dismissed_vulnerabilities) push_frontend_feature_flag(:junit_pipeline_view) end diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 38ca12e6f90..3810041b2af 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -195,7 +195,7 @@ module GitlabRoutingHelper end def snippet_path(snippet, *args) - if snippet.is_a?(ProjectSnippet) + if snippet.type == "ProjectSnippet" application_url_helpers.project_snippet_path(snippet.project, snippet, *args) else new_args = snippet_query_params(snippet, *args) @@ -204,7 +204,7 @@ module GitlabRoutingHelper end def snippet_url(snippet, *args) - if snippet.is_a?(ProjectSnippet) + if snippet.type == "ProjectSnippet" application_url_helpers.project_snippet_url(snippet.project, snippet, *args) else new_args = snippet_query_params(snippet, *args) @@ -213,7 +213,7 @@ module GitlabRoutingHelper end def raw_snippet_path(snippet, *args) - if snippet.is_a?(ProjectSnippet) + if snippet.type == "ProjectSnippet" application_url_helpers.raw_project_snippet_path(snippet.project, snippet, *args) else new_args = snippet_query_params(snippet, *args) @@ -222,7 +222,7 @@ module GitlabRoutingHelper end def raw_snippet_url(snippet, *args) - if snippet.is_a?(ProjectSnippet) + if snippet.type == "ProjectSnippet" application_url_helpers.raw_project_snippet_url(snippet.project, snippet, *args) else new_args = snippet_query_params(snippet, *args) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 777fe82e4c0..a89fea4b7b8 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -31,13 +31,14 @@ module SearchHelper from = collection.offset_value + 1 to = collection.offset_value + collection.to_a.size count = collection.total_count + term_element = " #{h(term)} ".html_safe search_entries_info_template(collection) % { from: from, to: to, count: count, scope: search_entries_scope_label(scope, count), - term: term + term_element: term_element } end @@ -72,9 +73,9 @@ module SearchHelper def search_entries_info_template(collection) if collection.total_pages > 1 - s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\"") + s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element}").html_safe else - s_("SearchResults|Showing %{count} %{scope} for \"%{term}\"") + s_("SearchResults|Showing %{count} %{scope} for%{term_element}").html_safe end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 5821cc1a1a5..c3292d7524e 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -204,7 +204,7 @@ module Ci end scope :internal, -> { where(source: internal_sources) } - scope :ci_sources, -> { where(config_source: ci_sources_values) } + scope :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) } scope :for_user, -> (user) { where(user: user) } scope :for_sha, -> (sha) { where(sha: sha) } scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) } @@ -315,10 +315,6 @@ module Ci sources.reject { |source| source == "external" }.values end - def self.ci_sources_values - config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source) - end - def self.bridgeable_statuses ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created preparing pending] end diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb index 859abc4a0d5..ac930f63abf 100644 --- a/app/models/ci/pipeline_enums.rb +++ b/app/models/ci/pipeline_enums.rb @@ -35,9 +35,20 @@ module Ci { unknown_source: nil, repository_source: 1, - auto_devops_source: 2 + auto_devops_source: 2, + remote_source: 4, + external_project_source: 5 } end + + def self.ci_config_sources_values + config_sources.values_at( + :unknown_source, + :repository_source, + :auto_devops_source, + :remote_source, + :external_project_source) + end end end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index f02ccd9e55e..48c96203921 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -58,7 +58,7 @@ class ProjectWiki end def wiki_base_path - [Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/wikis'].join('') + [Gitlab.config.gitlab.relative_url_root, '/', @project.full_path, '/-', '/wikis'].join('') end # Returns the Gitlab::Git::Wiki object. diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index eda7a36c2ee..2a81931c49f 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -3,6 +3,9 @@ class MergeRequestWidgetEntity < Grape::Entity include RequestAwareEntity + expose :id + expose :iid + expose :source_project_full_path do |merge_request| merge_request.source_project&.full_path end @@ -65,6 +68,8 @@ class MergeRequestWidgetEntity < Grape::Entity end def as_json(options = {}) + return super(options) if Feature.enabled?(:async_mr_widget) + super(options) .merge(MergeRequestPollCachedWidgetEntity.new(object, **@options.opts_hash).as_json(options)) .merge(MergeRequestPollWidgetEntity.new(object, **@options.opts_hash).as_json(options)) diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml index ed9b3ab1940..4244556a24a 100644 --- a/app/views/ci/variables/_variable_row.html.haml +++ b/app/views/ci/variables/_variable_row.html.haml @@ -44,31 +44,21 @@ .ci-variable-body-item.ci-variable-protected-item.table-section.section-20.mr-0.border-top-0 .append-right-default = s_("CiVariable|Protected") - %button{ type: 'button', - class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if is_protected}", - "aria-label": s_("CiVariable|Toggle protected") } + = render "shared/buttons/project_feature_toggle", is_checked: is_protected, label: s_("CiVariable|Toggle protected") do %input{ type: "hidden", class: 'js-ci-variable-input-protected js-project-feature-toggle-input', name: protected_input_name, value: is_protected, data: { default: is_protected_default.to_s } } - %span.toggle-icon - = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') - = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') .ci-variable-body-item.ci-variable-masked-item.table-section.section-20.mr-0.border-top-0 .append-right-default = s_("CiVariable|Masked") - %button{ type: 'button', - class: "js-project-feature-toggle project-feature-toggle qa-variable-masked #{'is-checked' if is_masked}", - "aria-label": s_("CiVariable|Toggle masked") } + = render "shared/buttons/project_feature_toggle", is_checked: is_masked, label: s_("CiVariable|Toggle masked"), class_list: "js-project-feature-toggle project-feature-toggle qa-variable-masked" do %input{ type: "hidden", class: 'js-ci-variable-input-masked js-project-feature-toggle-input', name: masked_input_name, value: is_masked, data: { default: is_masked_default.to_s } } - %span.toggle-icon - = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') - = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') = render_if_exists 'ci/variables/environment_scope', form_field: form_field, variable: variable %button.js-row-remove-button.ci-variable-row-remove-button.table-section.section-5.border-top-0{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } = icon('minus-circle') diff --git a/app/views/clusters/clusters/_form.html.haml b/app/views/clusters/clusters/_form.html.haml index 3d0266a2d5b..f9085b781fb 100644 --- a/app/views/clusters/clusters/_form.html.haml +++ b/app/views/clusters/clusters/_form.html.haml @@ -3,14 +3,8 @@ .form-group %h5= s_('ClusterIntegration|Integration status') %label.append-bottom-0.js-cluster-enable-toggle-area - %button{ type: 'button', - class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}", - "aria-label": s_("ClusterIntegration|Toggle Kubernetes cluster"), - disabled: !can?(current_user, :update_cluster, @cluster) } + = render "shared/buttons/project_feature_toggle", is_checked: @cluster.enabled?, label: s_("ClusterIntegration|Toggle Kubernetes cluster"), disabled: !can?(current_user, :update_cluster, @cluster) do = field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'} - %span.toggle-icon - = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') - = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.') .form-group diff --git a/app/views/shared/buttons/_project_feature_toggle.html.haml b/app/views/shared/buttons/_project_feature_toggle.html.haml new file mode 100644 index 00000000000..0f630786455 --- /dev/null +++ b/app/views/shared/buttons/_project_feature_toggle.html.haml @@ -0,0 +1,16 @@ +- class_list ||= "js-project-feature-toggle project-feature-toggle" +- data ||= nil +- disabled ||= false +- is_checked ||= false +- label ||= nil + +%button{ type: 'button', + class: "#{class_list} #{'is-disabled' if disabled} #{'is-checked' if is_checked}", + "aria-label": label, + disabled: disabled, + data: data } + - if yield.present? + = yield + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') diff --git a/changelogs/unreleased/30666-fix-sr-term-style.yml b/changelogs/unreleased/30666-fix-sr-term-style.yml new file mode 100644 index 00000000000..e69a7f49ae8 --- /dev/null +++ b/changelogs/unreleased/30666-fix-sr-term-style.yml @@ -0,0 +1,5 @@ +--- +title: Changes to how the search term is styled in the results +merge_request: 20416 +author: +type: changed diff --git a/changelogs/unreleased/35570-add-to-the-environment-view-pod-state-legend.yml b/changelogs/unreleased/35570-add-to-the-environment-view-pod-state-legend.yml new file mode 100644 index 00000000000..8949eb6881a --- /dev/null +++ b/changelogs/unreleased/35570-add-to-the-environment-view-pod-state-legend.yml @@ -0,0 +1,5 @@ +--- +title: Added legend to deploy boards +merge_request: 20208 +author: +type: added diff --git a/changelogs/unreleased/37403-npm-install-fails-with-ci_job_token-in-npmrc.yml b/changelogs/unreleased/37403-npm-install-fails-with-ci_job_token-in-npmrc.yml new file mode 100644 index 00000000000..65ace335e1e --- /dev/null +++ b/changelogs/unreleased/37403-npm-install-fails-with-ci_job_token-in-npmrc.yml @@ -0,0 +1,5 @@ +--- +title: Allow NPM package downloads with CI_JOB_TOKEN +merge_request: 20868 +author: +type: added diff --git a/changelogs/unreleased/Delete-board_service-js-in-app-folder.yml b/changelogs/unreleased/Delete-board_service-js-in-app-folder.yml new file mode 100644 index 00000000000..52027fa603f --- /dev/null +++ b/changelogs/unreleased/Delete-board_service-js-in-app-folder.yml @@ -0,0 +1,5 @@ +--- +title: delete board_service.js +merge_request: 20168 +author: nuwe1 +type: other diff --git a/changelogs/unreleased/add-root-ci-config-including-user-defined-config.yml b/changelogs/unreleased/add-root-ci-config-including-user-defined-config.yml new file mode 100644 index 00000000000..3b292bf29e5 --- /dev/null +++ b/changelogs/unreleased/add-root-ci-config-including-user-defined-config.yml @@ -0,0 +1,5 @@ +--- +title: Allow CI config path to point to a URL or file in a different repository +merge_request: 20179 +author: +type: added diff --git a/changelogs/unreleased/dz-move-wiki-route.yml b/changelogs/unreleased/dz-move-wiki-route.yml new file mode 100644 index 00000000000..f4fa96d8822 --- /dev/null +++ b/changelogs/unreleased/dz-move-wiki-route.yml @@ -0,0 +1,5 @@ +--- +title: Move wiki routing under /-/ scope +merge_request: 21185 +author: +type: deprecated diff --git a/changelogs/unreleased/fj-38068-fix-snippet-route-refactor.yml b/changelogs/unreleased/fj-38068-fix-snippet-route-refactor.yml new file mode 100644 index 00000000000..b3325a81882 --- /dev/null +++ b/changelogs/unreleased/fj-38068-fix-snippet-route-refactor.yml @@ -0,0 +1,5 @@ +--- +title: Fix snippet routes +merge_request: 21248 +author: +type: fixed diff --git a/changelogs/unreleased/ph-31406-fetchWidgetDataAsync.yml b/changelogs/unreleased/ph-31406-fetchWidgetDataAsync.yml new file mode 100644 index 00000000000..0046416978a --- /dev/null +++ b/changelogs/unreleased/ph-31406-fetchWidgetDataAsync.yml @@ -0,0 +1,5 @@ +--- +title: Fetches initial merge request widget data async +merge_request: 20719 +author: +type: changed diff --git a/config/routes/project.rb b/config/routes/project.rb index 8a5e20c8eff..2cc04da6f35 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -258,6 +258,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do post :list_projects end end + + # The wiki routing contains wildcard characters so + # its preferable to keep it below all other project routes + draw :wiki end # End of the /-/ scope. @@ -523,9 +527,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do post :web_ide_clientside_preview end - # Since both wiki and repository routing contains wildcard characters + # The repository routing contains wildcard characters so # its preferable to keep it below all other project routes - draw :wiki draw :repository # All new routes should go under /-/ scope. @@ -542,7 +545,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do :forks, :group_links, :import, :avatar, :mirror, :cycle_analytics, :mattermost, :variables, :triggers, :environments, :protected_environments, :error_tracking, - :serverless, :clusters, :audit_events) + :serverless, :clusters, :audit_events, :wikis) end # rubocop: disable Cop/PutProjectRoutesUnderScope diff --git a/config/webpack.config.js b/config/webpack.config.js index 3a43f515a1b..c0be2f66ca7 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -185,7 +185,7 @@ module.exports = { options: { limit: 2048 }, }, { - test: /\_worker\.js$/, + test: /_worker\.js$/, use: [ { loader: 'worker-loader', diff --git a/db/post_migrate/20191108202723_add_unique_constraint_to_software_licenses.rb b/db/post_migrate/20191108202723_add_unique_constraint_to_software_licenses.rb new file mode 100644 index 00000000000..580d3a189c8 --- /dev/null +++ b/db/post_migrate/20191108202723_add_unique_constraint_to_software_licenses.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +class AddUniqueConstraintToSoftwareLicenses < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + DOWNTIME = false + NEW_INDEX = 'index_software_licenses_on_unique_name' + OLD_INDEX = 'index_software_licenses_on_name' + + disable_ddl_transaction! + + # 12 software licenses will be removed on GitLab.com + # 0 software license policies will be updated on GitLab.com + def up(attempts: 100) + remove_redundant_software_licenses! + + add_concurrent_index :software_licenses, :name, unique: true, name: NEW_INDEX + remove_concurrent_index :software_licenses, :name, name: OLD_INDEX + rescue ActiveRecord::RecordNotUnique + retry if (attempts -= 1) > 0 + + raise StandardError, <<~EOS + Failed to add an unique index to software_licenses, despite retrying the + migration 100 times. + + See https://gitlab.com/gitlab-org/gitlab/merge_requests/19840. + EOS + end + + def down + remove_concurrent_index :software_licenses, :name, unique: true, name: NEW_INDEX + add_concurrent_index :software_licenses, :name, name: OLD_INDEX + end + + private + + def remove_redundant_software_licenses! + redundant_software_licenses = execute <<~SQL + SELECT min(id) id, name + FROM software_licenses + WHERE name IN (select name from software_licenses group by name having count(name) > 1) + GROUP BY name + SQL + say "Detected #{redundant_software_licenses.count} duplicates." + + redundant_software_licenses.each_row do |id, name| + say_with_time("Reassigning policies that reference software license #{name}.") do + duplicates = software_licenses.where.not(id: id).where(name: name) + + software_license_policies + .where(software_license_id: duplicates) + .update_all(software_license_id: id) + + duplicates.delete_all + end + end + end + + def table(name) + Class.new(ActiveRecord::Base) { self.table_name = name } + end + + def software_licenses + @software_licenses ||= table(:software_licenses) + end + + def software_license_policies + @software_license_policies ||= table(:software_license_policies) + end +end diff --git a/db/schema.rb b/db/schema.rb index c6fa84e96b5..621ae7f380e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -3698,7 +3698,7 @@ ActiveRecord::Schema.define(version: 2019_12_02_031812) do create_table "software_licenses", id: :serial, force: :cascade do |t| t.string "name", null: false t.string "spdx_identifier", limit: 255 - t.index ["name"], name: "index_software_licenses_on_name" + t.index ["name"], name: "index_software_licenses_on_unique_name", unique: true t.index ["spdx_identifier"], name: "index_software_licenses_on_spdx_identifier" end diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 2f2ab65c5dc..1e229db8b2e 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -969,7 +969,7 @@ X-Gitlab-Event: Wiki Page Hook "http_url": "http://example.com/root/awesome-project.git" }, "wiki": { - "web_url": "http://example.com/root/awesome-project/wikis/home", + "web_url": "http://example.com/root/awesome-project/-/wikis/home", "git_ssh_url": "git@example.com:root/awesome-project.wiki.git", "git_http_url": "http://example.com/root/awesome-project.wiki.git", "path_with_namespace": "root/awesome-project.wiki", @@ -981,7 +981,7 @@ X-Gitlab-Event: Wiki Page Hook "format": "markdown", "message": "adding an awesome page to the wiki", "slug": "awesome", - "url": "http://example.com/root/awesome-project/wikis/awesome", + "url": "http://example.com/root/awesome-project/-/wikis/awesome", "action": "create" } } diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md index 6480c7e0af9..a63da5c442f 100644 --- a/doc/user/project/pipelines/settings.md +++ b/doc/user/project/pipelines/settings.md @@ -67,20 +67,37 @@ For information about setting a maximum artifact size for a project, see ## Custom CI configuration path -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12509) in GitLab 9.4. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/12509) in GitLab 9.4. +> - [Support for external `.gitlab-ci.yml` locations](https://gitlab.com/gitlab-org/gitlab/issues/14376) introduced in GitLab 12.6. By default we look for the `.gitlab-ci.yml` file in the project's root -directory. If you require a different location **within** the repository, -you can set a custom path that will be used to look up the configuration file, -this path should be **relative** to the root. +directory. If needed, you can specify an alternate path and file name, including locations outside the project. -Here are some valid examples: +Hosting the configuration file in a separate project will allow stricter control of the +configuration file. You can limit access to the project hosting the configuration to only people +with proper authorization, and users can use the configuration for their pipelines, +without being able to modify it. -- `.gitlab-ci.yml` +If the CI configuration will stay within the repository, but in a +location different than the default, +the path must be relative to the root directory. Examples of valid paths and file names: + +- `.gitlab-ci.yml` (default) - `.my-custom-file.yml` - `my/path/.gitlab-ci.yml` - `my/path/.my-custom-file.yml` +If the CI configuration will be hosted in a different project within GitLab, the path must be relative +to the root directory in the other project, with the group and project name added to the end: + +- `.gitlab-ci.yml@mygroup/another-project` +- `my/path/.my-custom-file.yml@mygroup/another-project` + +If the CI configuration will be hosted on an external site, different than the GitLab instance, +the URL link must end with `.yml`: + +- `http://example.com/generate/ci/config.yml` + The path can be customized at a project level. To customize the path: 1. Go to the project's **Settings > CI / CD**. diff --git a/lib/gitlab/ci/pipeline/chain/config/content.rb b/lib/gitlab/ci/pipeline/chain/config/content.rb index a8cd99b8e92..d4b7444005e 100644 --- a/lib/gitlab/ci/pipeline/chain/config/content.rb +++ b/lib/gitlab/ci/pipeline/chain/config/content.rb @@ -8,21 +8,28 @@ module Gitlab class Content < Chain::Base include Chain::Helpers + SOURCES = [ + Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime, + Gitlab::Ci::Pipeline::Chain::Config::Content::Repository, + Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject, + Gitlab::Ci::Pipeline::Chain::Config::Content::Remote, + Gitlab::Ci::Pipeline::Chain::Config::Content::AutoDevops + ].freeze + + LEGACY_SOURCES = [ + Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime, + Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyRepository, + Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops + ].freeze + def perform! - return if @command.config_content - - if content = content_from_repo - @command.config_content = content - @pipeline.config_source = :repository_source - # TODO: we should persist ci_config_path - # @pipeline.config_path = ci_config_path - elsif content = content_from_auto_devops - @command.config_content = content - @pipeline.config_source = :auto_devops_source - end - - unless @command.config_content - return error("Missing #{ci_config_path} file") + if config = find_config + # TODO: we should persist config_content + # @pipeline.config_content = config.content + @command.config_content = config.content + @pipeline.config_source = config.source + else + error('Missing CI config file') end end @@ -32,24 +39,21 @@ module Gitlab private - def content_from_repo - return unless project - return unless @pipeline.sha - return unless ci_config_path + def find_config + sources.each do |source| + config = source.new(@pipeline, @command) + return config if config.exists? + end - project.repository.gitlab_ci_yml_for(@pipeline.sha, ci_config_path) - rescue GRPC::NotFound, GRPC::Internal nil end - def content_from_auto_devops - return unless project&.auto_devops_enabled? - - Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content - end - - def ci_config_path - project.ci_config_path.presence || '.gitlab-ci.yml' + def sources + if Feature.enabled?(:ci_root_config_content, @command.project, default_enabled: true) + SOURCES + else + LEGACY_SOURCES + end end end end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb new file mode 100644 index 00000000000..e9bcc67de9c --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/config/content/auto_devops.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Config + class Content + class AutoDevops < Source + def content + strong_memoize(:content) do + next unless project&.auto_devops_enabled? + + template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') + YAML.dump('include' => [{ 'template' => template.full_name }]) + end + end + + def source + :auto_devops_source + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/external_project.rb b/lib/gitlab/ci/pipeline/chain/config/content/external_project.rb new file mode 100644 index 00000000000..8a19e433483 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/config/content/external_project.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Config + class Content + class ExternalProject < Source + def content + strong_memoize(:content) do + next unless external_project_path? + + path_file, path_project = ci_config_path.split('@', 2) + YAML.dump('include' => [{ 'project' => path_project, 'file' => path_file }]) + end + end + + def source + :external_project_source + end + + private + + # Example: path/to/.gitlab-ci.yml@another-group/another-project + def external_project_path? + ci_config_path =~ /\A.+(yml|yaml)@.+\z/ + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb new file mode 100644 index 00000000000..c4cef356628 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/config/content/legacy_auto_devops.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Config + class Content + class LegacyAutoDevops < Source + def content + strong_memoize(:content) do + next unless project&.auto_devops_enabled? + + template = Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps') + template.content + end + end + + def source + :auto_devops_source + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/legacy_repository.rb b/lib/gitlab/ci/pipeline/chain/config/content/legacy_repository.rb new file mode 100644 index 00000000000..fa4a97c6880 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/config/content/legacy_repository.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Config + class Content + class LegacyRepository < Source + def content + strong_memoize(:content) do + next unless project + next unless @pipeline.sha + next unless ci_config_path + + project.repository.gitlab_ci_yml_for(@pipeline.sha, ci_config_path) + rescue GRPC::NotFound, GRPC::Internal + nil + end + end + + def source + :repository_source + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/remote.rb b/lib/gitlab/ci/pipeline/chain/config/content/remote.rb new file mode 100644 index 00000000000..dcc336b8929 --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/config/content/remote.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Config + class Content + class Remote < Source + def content + strong_memoize(:content) do + next unless ci_config_path =~ URI.regexp(%w[http https]) + + YAML.dump('include' => [{ 'remote' => ci_config_path }]) + end + end + + def source + :remote_source + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/repository.rb b/lib/gitlab/ci/pipeline/chain/config/content/repository.rb new file mode 100644 index 00000000000..0752b099d3d --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/config/content/repository.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Config + class Content + class Repository < Source + def content + strong_memoize(:content) do + next unless file_in_repository? + + YAML.dump('include' => [{ 'local' => ci_config_path }]) + end + end + + def source + :repository_source + end + + private + + def file_in_repository? + return unless project + return unless @pipeline.sha + + project.repository.gitlab_ci_yml_for(@pipeline.sha, ci_config_path).present? + rescue GRPC::NotFound, GRPC::Internal + nil + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/runtime.rb b/lib/gitlab/ci/pipeline/chain/config/content/runtime.rb new file mode 100644 index 00000000000..4811d3d913d --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/config/content/runtime.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Config + class Content + class Runtime < Source + def content + @command.config_content + end + + def source + # The only case when this source is used is when the config content + # is passed in as parameter to Ci::CreatePipelineService. + # This would only occur with parent/child pipelines which is being + # implemented. + # TODO: change source to return :runtime_source + # https://gitlab.com/gitlab-org/gitlab/merge_requests/21041 + + nil + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/chain/config/content/source.rb b/lib/gitlab/ci/pipeline/chain/config/content/source.rb new file mode 100644 index 00000000000..3389187473b --- /dev/null +++ b/lib/gitlab/ci/pipeline/chain/config/content/source.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Chain + module Config + class Content + class Source + include Gitlab::Utils::StrongMemoize + + DEFAULT_YAML_FILE = '.gitlab-ci.yml' + + def initialize(pipeline, command) + @pipeline = pipeline + @command = command + end + + def exists? + strong_memoize(:exists) do + content.present? + end + end + + def content + raise NotImplementedError + end + + def source + raise NotImplementedError + end + + def project + @project ||= @pipeline.project + end + + def ci_config_path + @ci_config_path ||= project.ci_config_path.presence || DEFAULT_YAML_FILE + end + end + end + end + end + end + end +end diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index c89b8f563dc..488e8e0fcea 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -105,7 +105,7 @@ module Gitlab save_id_mapping(relation_key, data_hash, relation_object) rescue => e # re-raise if not enabled - raise e unless Feature.enabled?(:import_graceful_failures, @project.group) + raise e unless Feature.enabled?(:import_graceful_failures, @project.group, default_enabled: true) log_import_failure(relation_key, relation_index, e) end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d556c699bd3..58b9c7d905a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2960,6 +2960,9 @@ msgstr "" msgid "Can't scan the code?" msgstr "" +msgid "Canary" +msgstr "" + msgid "Canary Deployments is a popular CI strategy, where a small portion of the fleet is updated to the new version of your application." msgstr "" @@ -15325,16 +15328,16 @@ msgstr "" msgid "SearchCodeResults|of %{link_to_project}" msgstr "" -msgid "SearchResults|Showing %{count} %{scope} for \"%{term}\"" +msgid "SearchResults|Showing %{count} %{scope} for%{term_element}" msgstr "" -msgid "SearchResults|Showing %{count} %{scope} for \"%{term}\" in your personal and project snippets" +msgid "SearchResults|Showing %{count} %{scope} for%{term_element} in your personal and project snippets" msgstr "" -msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\"" +msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element}" msgstr "" -msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for \"%{term}\" in your personal and project snippets" +msgid "SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element} in your personal and project snippets" msgstr "" msgid "SearchResults|We couldn't find any %{scope} matching %{term}" @@ -16963,6 +16966,9 @@ msgstr "" msgid "Subtracts" msgstr "" +msgid "Succeeded" +msgstr "" + msgid "Successfully activated" msgstr "" @@ -18760,6 +18766,9 @@ msgstr "" msgid "Unable to load the diff. %{button_try_again}" msgstr "" +msgid "Unable to load the merge request widget. Try reloading the page." +msgstr "" + msgid "Unable to resolve" msgstr "" diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 4519cd014a1..ef5f286502b 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1073,7 +1073,7 @@ describe Projects::MergeRequestsController do end it 'renders MergeRequest as JSON' do - expect(json_response.keys).to include('id', 'iid', 'description') + expect(json_response.keys).to include('id', 'iid') end end @@ -1107,7 +1107,7 @@ describe Projects::MergeRequestsController do it 'renders MergeRequest as JSON' do subject - expect(json_response.keys).to include('id', 'iid', 'description') + expect(json_response.keys).to include('id', 'iid') end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index f6eeb8d7065..f5558a1f2ec 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -706,7 +706,7 @@ describe 'Pipelines', :js do click_on 'Run Pipeline' end - it { expect(page).to have_content('Missing .gitlab-ci.yml file') } + it { expect(page).to have_content('Missing CI config file') } it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again' do click_button project.default_branch diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 5c6b04a7141..331ba58d067 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -29,11 +29,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do expect(page).to have_content("regular link") - expect(page.html).to include("regular link") - expect(page.html).to include("relative link 1") - expect(page.html).to include("relative link 2") - expect(page.html).to include("relative link 3") - expect(page.html).to include("spaced link") + expect(page.html).to include("regular link") + expect(page.html).to include("relative link 1") + expect(page.html).to include("relative link 2") + expect(page.html).to include("relative link 3") + expect(page.html).to include("spaced link") end end @@ -43,11 +43,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do expect(page).to have_content("regular link") - expect(page.html).to include("regular link") - expect(page.html).to include("relative link 1") - expect(page.html).to include("relative link 2") - expect(page.html).to include("relative link 3") - expect(page.html).to include("spaced link") + expect(page.html).to include("regular link") + expect(page.html).to include("relative link 1") + expect(page.html).to include("relative link 2") + expect(page.html).to include("relative link 3") + expect(page.html).to include("spaced link") end end @@ -57,11 +57,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do expect(page).to have_content("regular link") - expect(page.html).to include("regular link") - expect(page.html).to include("relative link 1") - expect(page.html).to include("relative link 2") - expect(page.html).to include("relative link 3") - expect(page.html).to include("spaced link") + expect(page.html).to include("regular link") + expect(page.html).to include("relative link 1") + expect(page.html).to include("relative link 2") + expect(page.html).to include("relative link 3") + expect(page.html).to include("spaced link") end end end @@ -77,11 +77,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do expect(page).to have_content("regular link") - expect(page.html).to include("regular link") - expect(page.html).to include("relative link 1") - expect(page.html).to include("relative link 2") - expect(page.html).to include("relative link 3") - expect(page.html).to include("spaced link") + expect(page.html).to include("regular link") + expect(page.html).to include("relative link 1") + expect(page.html).to include("relative link 2") + expect(page.html).to include("relative link 3") + expect(page.html).to include("spaced link") end end @@ -95,11 +95,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do expect(page).to have_content("regular link") - expect(page.html).to include("regular link") - expect(page.html).to include("relative link 1") - expect(page.html).to include("relative link 2") - expect(page.html).to include("relative link 3") - expect(page.html).to include("spaced link") + expect(page.html).to include("regular link") + expect(page.html).to include("relative link 1") + expect(page.html).to include("relative link 2") + expect(page.html).to include("relative link 3") + expect(page.html).to include("spaced link") end end @@ -113,11 +113,11 @@ describe 'Projects > Wiki > User previews markdown changes', :js do expect(page).to have_content("regular link") - expect(page.html).to include("regular link") - expect(page.html).to include("relative link 1") - expect(page.html).to include("relative link 2") - expect(page.html).to include("relative link 3") - expect(page.html).to include("spaced link") + expect(page.html).to include("regular link") + expect(page.html).to include("relative link 1") + expect(page.html).to include("relative link 2") + expect(page.html).to include("relative link 3") + expect(page.html).to include("spaced link") end end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 56d0518015d..499c459621a 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -55,7 +55,7 @@ describe "User creates wiki page" do end expect(current_path).to include("one/two/three-test") - expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']") + expect(page).to have_xpath("//a[@href='/#{project.full_path}/-/wikis/one/two/three-test']") end it "has `Create home` as a commit message", :js do diff --git a/spec/frontend/boards/services/board_service_spec.js b/spec/frontend/boards/boards_store_spec.js similarity index 84% rename from spec/frontend/boards/services/board_service_spec.js rename to spec/frontend/boards/boards_store_spec.js index 86f49f63f4e..f8e5873c87b 100644 --- a/spec/frontend/boards/services/board_service_spec.js +++ b/spec/frontend/boards/boards_store_spec.js @@ -1,10 +1,9 @@ -import BoardService from '~/boards/services/board_service'; import { TEST_HOST } from 'helpers/test_constants'; import AxiosMockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import boardsStore from '~/boards/stores/boards_store'; -describe('BoardService', () => { +describe('boardsStore', () => { const dummyResponse = "without type checking this doesn't matter"; const boardId = 'dummy-board-id'; const endpoints = { @@ -14,7 +13,6 @@ describe('BoardService', () => { recentBoardsEndpoint: `${TEST_HOST}/recent/boards`, }; - let service; let axiosMock; beforeEach(() => { @@ -23,7 +21,6 @@ describe('BoardService', () => { ...endpoints, boardId, }); - service = new BoardService(); }); describe('all', () => { @@ -31,13 +28,13 @@ describe('BoardService', () => { axiosMock.onGet(endpoints.listsEndpoint).replyOnce(200, dummyResponse); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.all()).resolves.toEqual(expectedResponse); + return expect(boardsStore.all()).resolves.toEqual(expectedResponse); }); it('fails for error response', () => { axiosMock.onGet(endpoints.listsEndpoint).replyOnce(500); - return expect(service.all()).rejects.toThrow(); + return expect(boardsStore.all()).rejects.toThrow(); }); }); @@ -48,13 +45,13 @@ describe('BoardService', () => { axiosMock.onPost(listsEndpointGenerate).replyOnce(200, dummyResponse); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.generateDefaultLists()).resolves.toEqual(expectedResponse); + return expect(boardsStore.generateDefaultLists()).resolves.toEqual(expectedResponse); }); it('fails for error response', () => { axiosMock.onPost(listsEndpointGenerate).replyOnce(500); - return expect(service.generateDefaultLists()).rejects.toThrow(); + return expect(boardsStore.generateDefaultLists()).rejects.toThrow(); }); }); @@ -76,7 +73,7 @@ describe('BoardService', () => { requestSpy.mockReturnValue([200, dummyResponse]); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.createList(entityId, entityType)) + return expect(boardsStore.createList(entityId, entityType)) .resolves.toEqual(expectedResponse) .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -86,7 +83,7 @@ describe('BoardService', () => { it('fails for error response', () => { requestSpy.mockReturnValue([500]); - return expect(service.createList(entityId, entityType)) + return expect(boardsStore.createList(entityId, entityType)) .rejects.toThrow() .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -113,7 +110,7 @@ describe('BoardService', () => { requestSpy.mockReturnValue([200, dummyResponse]); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.updateList(id, position, collapsed)) + return expect(boardsStore.updateList(id, position, collapsed)) .resolves.toEqual(expectedResponse) .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -123,7 +120,7 @@ describe('BoardService', () => { it('fails for error response', () => { requestSpy.mockReturnValue([500]); - return expect(service.updateList(id, position, collapsed)) + return expect(boardsStore.updateList(id, position, collapsed)) .rejects.toThrow() .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -147,7 +144,7 @@ describe('BoardService', () => { requestSpy.mockReturnValue([200, dummyResponse]); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.destroyList(id)) + return expect(boardsStore.destroyList(id)) .resolves.toEqual(expectedResponse) .then(() => { expect(requestSpy).toHaveBeenCalled(); @@ -157,7 +154,7 @@ describe('BoardService', () => { it('fails for error response', () => { requestSpy.mockReturnValue([500]); - return expect(service.destroyList(id)) + return expect(boardsStore.destroyList(id)) .rejects.toThrow() .then(() => { expect(requestSpy).toHaveBeenCalled(); @@ -173,7 +170,7 @@ describe('BoardService', () => { axiosMock.onGet(url).replyOnce(200, dummyResponse); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.getIssuesForList(id)).resolves.toEqual(expectedResponse); + return expect(boardsStore.getIssuesForList(id)).resolves.toEqual(expectedResponse); }); it('makes a request to fetch list issues with filter', () => { @@ -181,13 +178,13 @@ describe('BoardService', () => { axiosMock.onGet(`${url}&algal=scrubber`).replyOnce(200, dummyResponse); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.getIssuesForList(id, filter)).resolves.toEqual(expectedResponse); + return expect(boardsStore.getIssuesForList(id, filter)).resolves.toEqual(expectedResponse); }); it('fails for error response', () => { axiosMock.onGet(url).replyOnce(500); - return expect(service.getIssuesForList(id)).rejects.toThrow(); + return expect(boardsStore.getIssuesForList(id)).rejects.toThrow(); }); }); @@ -228,7 +225,7 @@ describe('BoardService', () => { requestSpy.mockReturnValue([200, dummyResponse]); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId)) + return expect(boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId)) .resolves.toEqual(expectedResponse) .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -238,7 +235,7 @@ describe('BoardService', () => { it('fails for error response', () => { requestSpy.mockReturnValue([500]); - return expect(service.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId)) + return expect(boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId)) .rejects.toThrow() .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -267,7 +264,7 @@ describe('BoardService', () => { requestSpy.mockReturnValue([200, dummyResponse]); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.newIssue(id, issue)) + return expect(boardsStore.newIssue(id, issue)) .resolves.toEqual(expectedResponse) .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -277,7 +274,7 @@ describe('BoardService', () => { it('fails for error response', () => { requestSpy.mockReturnValue([500]); - return expect(service.newIssue(id, issue)) + return expect(boardsStore.newIssue(id, issue)) .rejects.toThrow() .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -304,13 +301,13 @@ describe('BoardService', () => { axiosMock.onGet(url).replyOnce(200, dummyResponse); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.getBacklog(requestParams)).resolves.toEqual(expectedResponse); + return expect(boardsStore.getBacklog(requestParams)).resolves.toEqual(expectedResponse); }); it('fails for error response', () => { axiosMock.onGet(url).replyOnce(500); - return expect(service.getBacklog(requestParams)).rejects.toThrow(); + return expect(boardsStore.getBacklog(requestParams)).rejects.toThrow(); }); }); @@ -337,7 +334,7 @@ describe('BoardService', () => { requestSpy.mockReturnValue([200, dummyResponse]); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.bulkUpdate(issueIds, extraData)) + return expect(boardsStore.bulkUpdate(issueIds, extraData)) .resolves.toEqual(expectedResponse) .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -347,7 +344,7 @@ describe('BoardService', () => { it('fails for error response', () => { requestSpy.mockReturnValue([500]); - return expect(service.bulkUpdate(issueIds, extraData)) + return expect(boardsStore.bulkUpdate(issueIds, extraData)) .rejects.toThrow() .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -362,13 +359,13 @@ describe('BoardService', () => { axiosMock.onGet(dummyEndpoint).replyOnce(200, dummyResponse); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(BoardService.getIssueInfo(dummyEndpoint)).resolves.toEqual(expectedResponse); + return expect(boardsStore.getIssueInfo(dummyEndpoint)).resolves.toEqual(expectedResponse); }); it('fails for error response', () => { axiosMock.onGet(dummyEndpoint).replyOnce(500); - return expect(BoardService.getIssueInfo(dummyEndpoint)).rejects.toThrow(); + return expect(boardsStore.getIssueInfo(dummyEndpoint)).rejects.toThrow(); }); }); @@ -379,7 +376,7 @@ describe('BoardService', () => { axiosMock.onPost(dummyEndpoint).replyOnce(200, dummyResponse); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(BoardService.toggleIssueSubscription(dummyEndpoint)).resolves.toEqual( + return expect(boardsStore.toggleIssueSubscription(dummyEndpoint)).resolves.toEqual( expectedResponse, ); }); @@ -387,7 +384,7 @@ describe('BoardService', () => { it('fails for error response', () => { axiosMock.onPost(dummyEndpoint).replyOnce(500); - return expect(BoardService.toggleIssueSubscription(dummyEndpoint)).rejects.toThrow(); + return expect(boardsStore.toggleIssueSubscription(dummyEndpoint)).rejects.toThrow(); }); }); @@ -398,13 +395,13 @@ describe('BoardService', () => { axiosMock.onGet(url).replyOnce(200, dummyResponse); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.allBoards()).resolves.toEqual(expectedResponse); + return expect(boardsStore.allBoards()).resolves.toEqual(expectedResponse); }); it('fails for error response', () => { axiosMock.onGet(url).replyOnce(500); - return expect(service.allBoards()).rejects.toThrow(); + return expect(boardsStore.allBoards()).rejects.toThrow(); }); }); @@ -415,13 +412,13 @@ describe('BoardService', () => { axiosMock.onGet(url).replyOnce(200, dummyResponse); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.recentBoards()).resolves.toEqual(expectedResponse); + return expect(boardsStore.recentBoards()).resolves.toEqual(expectedResponse); }); it('fails for error response', () => { axiosMock.onGet(url).replyOnce(500); - return expect(service.recentBoards()).rejects.toThrow(); + return expect(boardsStore.recentBoards()).rejects.toThrow(); }); }); @@ -462,7 +459,7 @@ describe('BoardService', () => { const expectedResponse = expect.objectContaining({ data: dummyResponse }); return expect( - service.createBoard({ + boardsStore.createBoard({ ...board, id, }), @@ -477,7 +474,7 @@ describe('BoardService', () => { requestSpy.mockReturnValue([500]); return expect( - service.createBoard({ + boardsStore.createBoard({ ...board, id, }), @@ -513,7 +510,7 @@ describe('BoardService', () => { requestSpy.mockReturnValue([200, dummyResponse]); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.createBoard(board)) + return expect(boardsStore.createBoard(board)) .resolves.toEqual(expectedResponse) .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -523,7 +520,7 @@ describe('BoardService', () => { it('fails for error response', () => { requestSpy.mockReturnValue([500]); - return expect(service.createBoard(board)) + return expect(boardsStore.createBoard(board)) .rejects.toThrow() .then(() => { expect(requestSpy).toHaveBeenCalledWith(expectedRequest); @@ -540,13 +537,13 @@ describe('BoardService', () => { axiosMock.onDelete(url).replyOnce(200, dummyResponse); const expectedResponse = expect.objectContaining({ data: dummyResponse }); - return expect(service.deleteBoard({ id })).resolves.toEqual(expectedResponse); + return expect(boardsStore.deleteBoard({ id })).resolves.toEqual(expectedResponse); }); it('fails for error response', () => { axiosMock.onDelete(url).replyOnce(500); - return expect(service.deleteBoard({ id })).rejects.toThrow(); + return expect(boardsStore.deleteBoard({ id })).rejects.toThrow(); }); }); }); diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index 28ab2f2d6c4..99869c46f3f 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -1,8 +1,7 @@ /* eslint no-param-reassign: "off" */ import $ from 'jquery'; -import { membersBeforeSave } from '~/gfm_auto_complete'; -import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; +import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete'; import 'jquery.caret'; import 'at.js'; diff --git a/spec/frontend/notes/components/note_app_spec.js b/spec/frontend/notes/components/note_app_spec.js index 3716b349210..3c960adb698 100644 --- a/spec/frontend/notes/components/note_app_spec.js +++ b/spec/frontend/notes/components/note_app_spec.js @@ -1,13 +1,13 @@ import $ from 'helpers/jquery'; import AxiosMockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; import Vue from 'vue'; import { mount, createLocalVue } from '@vue/test-utils'; +import { setTestTimeout } from 'helpers/timeout'; +import axios from '~/lib/utils/axios_utils'; import NotesApp from '~/notes/components/notes_app.vue'; import service from '~/notes/services/notes_service'; import createStore from '~/notes/stores'; import '~/behaviors/markdown/render_gfm'; -import { setTestTimeout } from 'helpers/timeout'; // TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491) import * as mockData from '../../notes/mock_data'; import * as urlUtility from '~/lib/utils/url_utility'; @@ -77,6 +77,8 @@ describe('note_app', () => { describe('set data', () => { beforeEach(() => { + setFixtures('
'); + axiosMock.onAny().reply(200, []); wrapper = mountComponent(); return waitForDiscussionsRequest(); @@ -97,6 +99,10 @@ describe('note_app', () => { it('should fetch discussions', () => { expect(store.state.discussions).toEqual([]); }); + + it('updates discussions badge', () => { + expect(document.querySelector('.js-discussions-count').textContent).toEqual('0'); + }); }); describe('render', () => { @@ -161,6 +167,7 @@ describe('note_app', () => { describe('while fetching data', () => { beforeEach(() => { + setFixtures('
'); axiosMock.onAny().reply(200, []); wrapper = mountComponent(); }); @@ -177,6 +184,10 @@ describe('note_app', () => { 'Write a comment or drag your files hereā€¦', ); }); + + it('should not update discussions badge (it should be blank)', () => { + expect(document.querySelector('.js-discussions-count').textContent).toEqual(''); + }); }); describe('update note', () => { diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index bef6fbe3d5f..18c94602596 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -122,13 +122,13 @@ describe SearchHelper do it 'uses the correct singular label' do collection = Kaminari.paginate_array([:foo]).page(1).per(10) - expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 #{label} for \"foo\"") + expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 #{label} for foo ") end it 'uses the correct plural label' do collection = Kaminari.paginate_array([:foo] * 23).page(1).per(10) - expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 - 10 of 23 #{label.pluralize} for \"foo\"") + expect(search_entries_info(collection, scope, 'foo')).to eq("Showing 1 - 10 of 23 #{label.pluralize} for foo ") end end diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb index bcc2bd71da1..1aab01281c6 100644 --- a/spec/helpers/wiki_helper_spec.rb +++ b/spec/helpers/wiki_helper_spec.rb @@ -27,7 +27,7 @@ describe WikiHelper do let(:classes) { "btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort" } def expected_link(sort, direction, icon_class) - path = "/#{project.full_path}/wikis/pages?direction=#{direction}&sort=#{sort}" + path = "/#{project.full_path}/-/wikis/pages?direction=#{direction}&sort=#{sort}" helper.link_to(path, type: 'button', class: classes, title: 'Sort direction') do helper.sprite_icon("sort-#{icon_class}", size: 16) diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js index 292fa745fe8..278930789e1 100644 --- a/spec/javascripts/boards/boards_store_spec.js +++ b/spec/javascripts/boards/boards_store_spec.js @@ -8,7 +8,6 @@ import '~/boards/models/label'; import '~/boards/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; -import '~/boards/services/board_service'; import boardsStore from '~/boards/stores/boards_store'; import eventHub from '~/boards/eventhub'; import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data'; diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js index 890a47c189a..181e7af7451 100644 --- a/spec/javascripts/boards/issue_spec.js +++ b/spec/javascripts/boards/issue_spec.js @@ -5,7 +5,6 @@ import '~/boards/models/label'; import '~/boards/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; -import '~/boards/services/board_service'; import boardsStore from '~/boards/stores/boards_store'; import { setMockEndpoints } from './mock_data'; diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js index 1fcd7958bc5..374e7d76b1c 100644 --- a/spec/javascripts/boards/list_spec.js +++ b/spec/javascripts/boards/list_spec.js @@ -10,7 +10,6 @@ import '~/boards/models/label'; import '~/boards/models/assignee'; import '~/boards/models/issue'; import '~/boards/models/list'; -import '~/boards/services/board_service'; import boardsStore from '~/boards/stores/boards_store'; import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data'; diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index ff34dafa660..ba999adb2a9 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -222,6 +222,7 @@ export default { plain_diff_path: '/root/acets-app/merge_requests/22.diff', merge_request_basic_path: '/root/acets-app/merge_requests/22.json?serializer=basic', merge_request_widget_path: '/root/acets-app/merge_requests/22/widget.json', + merge_request_cached_widget_path: '/cached.json', merge_check_path: '/root/acets-app/merge_requests/22/merge_check', ci_environments_status_url: '/root/acets-app/merge_requests/22/ci_environments_status', project_archived: false, diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 604b21e77fe..36e4ce7177f 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; import notify from '~/lib/utils/notify'; @@ -17,6 +19,7 @@ const returnPromise = data => describe('mrWidgetOptions', () => { let vm; + let mock; let MrWidgetOptions; const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch'; @@ -25,6 +28,13 @@ describe('mrWidgetOptions', () => { // Prevent component mounting delete mrWidgetOptions.el; + gl.mrWidgetData = { ...mockData }; + gon.features = { asyncMrWidget: true }; + + mock = new MockAdapter(axios); + mock.onGet(mockData.merge_request_widget_path).reply(() => [200, { ...mockData }]); + mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, { ...mockData }]); + MrWidgetOptions = Vue.extend(mrWidgetOptions); vm = mountComponent(MrWidgetOptions, { mrData: { ...mockData }, @@ -33,6 +43,9 @@ describe('mrWidgetOptions', () => { afterEach(() => { vm.$destroy(); + mock.restore(); + gl.mrWidgetData = {}; + gon.features = {}; }); describe('data', () => { @@ -308,59 +321,61 @@ describe('mrWidgetOptions', () => { }); describe('bindEventHubListeners', () => { - it('should bind eventHub listeners', () => { + it('should bind eventHub listeners', done => { spyOn(vm, 'checkStatus').and.returnValue(() => {}); spyOn(vm.service, 'checkStatus').and.returnValue(returnPromise(mockData)); spyOn(vm, 'fetchActionsContent'); spyOn(vm.mr, 'setData'); spyOn(vm, 'resumePolling'); spyOn(vm, 'stopPolling'); - spyOn(eventHub, '$on'); + spyOn(eventHub, '$on').and.callThrough(); - vm.bindEventHubListeners(); + setTimeout(() => { + eventHub.$emit('SetBranchRemoveFlag', ['flag']); - eventHub.$emit('SetBranchRemoveFlag', ['flag']); + expect(vm.mr.isRemovingSourceBranch).toEqual('flag'); - expect(vm.mr.isRemovingSourceBranch).toEqual('flag'); + eventHub.$emit('FailedToMerge'); - eventHub.$emit('FailedToMerge'); + expect(vm.mr.state).toEqual('failedToMerge'); - expect(vm.mr.state).toEqual('failedToMerge'); + eventHub.$emit('UpdateWidgetData', mockData); - eventHub.$emit('UpdateWidgetData', mockData); + expect(vm.mr.setData).toHaveBeenCalledWith(mockData); - expect(vm.mr.setData).toHaveBeenCalledWith(mockData); + eventHub.$emit('EnablePolling'); - eventHub.$emit('EnablePolling'); + expect(vm.resumePolling).toHaveBeenCalled(); - expect(vm.resumePolling).toHaveBeenCalled(); + eventHub.$emit('DisablePolling'); - eventHub.$emit('DisablePolling'); + expect(vm.stopPolling).toHaveBeenCalled(); - expect(vm.stopPolling).toHaveBeenCalled(); + const listenersWithServiceRequest = { + MRWidgetUpdateRequested: true, + FetchActionsContent: true, + }; - const listenersWithServiceRequest = { - MRWidgetUpdateRequested: true, - FetchActionsContent: true, - }; + const allArgs = eventHub.$on.calls.allArgs(); + allArgs.forEach(params => { + const eventName = params[0]; + const callback = params[1]; - const allArgs = eventHub.$on.calls.allArgs(); - allArgs.forEach(params => { - const eventName = params[0]; - const callback = params[1]; + if (listenersWithServiceRequest[eventName]) { + listenersWithServiceRequest[eventName] = callback; + } + }); - if (listenersWithServiceRequest[eventName]) { - listenersWithServiceRequest[eventName] = callback; - } + listenersWithServiceRequest.MRWidgetUpdateRequested(); + + expect(vm.checkStatus).toHaveBeenCalled(); + + listenersWithServiceRequest.FetchActionsContent(); + + expect(vm.fetchActionsContent).toHaveBeenCalled(); + + done(); }); - - listenersWithServiceRequest.MRWidgetUpdateRequested(); - - expect(vm.checkStatus).toHaveBeenCalled(); - - listenersWithServiceRequest.FetchActionsContent(); - - expect(vm.fetchActionsContent).toHaveBeenCalled(); }); }); @@ -451,22 +466,30 @@ describe('mrWidgetOptions', () => { }); describe('resumePolling', () => { - it('should call stopTimer on pollingInterval', () => { - spyOn(vm.pollingInterval, 'resume'); + it('should call stopTimer on pollingInterval', done => { + setTimeout(() => { + spyOn(vm.pollingInterval, 'resume'); - vm.resumePolling(); + vm.resumePolling(); - expect(vm.pollingInterval.resume).toHaveBeenCalled(); + expect(vm.pollingInterval.resume).toHaveBeenCalled(); + + done(); + }); }); }); describe('stopPolling', () => { - it('should call stopTimer on pollingInterval', () => { - spyOn(vm.pollingInterval, 'stopTimer'); + it('should call stopTimer on pollingInterval', done => { + setTimeout(() => { + spyOn(vm.pollingInterval, 'stopTimer'); - vm.stopPolling(); + vm.stopPolling(); - expect(vm.pollingInterval.stopTimer).toHaveBeenCalled(); + expect(vm.pollingInterval.stopTimer).toHaveBeenCalled(); + + done(); + }); }); }); }); diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 26f2b0b0acf..e1814ea403e 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -72,14 +72,14 @@ describe Banzai::Pipeline::WikiPipeline do markdown = "[Page](./page)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/page\"") end it "rewrites file links to be at the scope of the current directory" do markdown = "[Link to Page](./page.md)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/page.md\"") end end @@ -88,14 +88,14 @@ describe Banzai::Pipeline::WikiPipeline do markdown = "[Link to Page](../page)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/page\"") end it "rewrites file links to be at the scope of the parent directory" do markdown = "[Link to Page](../page.md)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/page.md\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/page.md\"") end end @@ -104,14 +104,14 @@ describe Banzai::Pipeline::WikiPipeline do markdown = "[Link to Page](./subdirectory/page)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/subdirectory/page\"") end it "rewrites file links to be at the scope of the sub-directory" do markdown = "[Link to Page](./subdirectory/page.md)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/subdirectory/page.md\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/subdirectory/page.md\"") end end @@ -120,35 +120,35 @@ describe Banzai::Pipeline::WikiPipeline do markdown = "[Link to Page](page)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page\"") end it 'rewrites non-file links (with spaces) to be at the scope of the wiki root' do markdown = "[Link to Page](page slug)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page%20slug\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page%20slug\"") end it "rewrites file links to be at the scope of the current directory" do markdown = "[Link to Page](page.md)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/nested/twice/page.md\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/nested/twice/page.md\"") end it 'rewrites links with anchor' do markdown = '[Link to Header](start-page#title)' output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start-page#title\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/start-page#title\"") end it 'rewrites links (with spaces) with anchor' do markdown = '[Link to Header](start page#title)' output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start%20page#title\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/start%20page#title\"") end end @@ -157,14 +157,14 @@ describe Banzai::Pipeline::WikiPipeline do markdown = "[Link to Page](/page)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page\"") end it 'rewrites file links to be at the scope of the wiki root' do markdown = "[Link to Page](/page.md)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page.md\"") + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/-/wikis/page.md\"") end end end @@ -270,28 +270,28 @@ describe Banzai::Pipeline::WikiPipeline do markdown = "![video_file](video_file_name.mp4)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) - expect(output).to include('