diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-14 18:08:45 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-02-14 18:08:45 +0000 |
commit | 26a50872e9da9509c52c70f74dc21698fec906db (patch) | |
tree | b1bd36bd72e701e346ef880fc7a905f6186525e7 | |
parent | b3a736ed88a1db0391cd9881e70b987bab7d89d1 (diff) | |
download | gitlab-ce-26a50872e9da9509c52c70f74dc21698fec906db.tar.gz |
Add latest changes from gitlab-org/gitlab@master
47 files changed, 858 insertions, 126 deletions
diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue new file mode 100644 index 00000000000..2639a099093 --- /dev/null +++ b/app/assets/javascripts/blob/components/blob_content.vue @@ -0,0 +1,51 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers'; +import BlobContentError from './blob_content_error.vue'; + +export default { + components: { + GlLoadingIcon, + BlobContentError, + }, + props: { + content: { + type: String, + default: '', + required: false, + }, + loading: { + type: Boolean, + default: true, + required: false, + }, + activeViewer: { + type: Object, + required: true, + }, + }, + computed: { + viewer() { + switch (this.activeViewer.type) { + case 'rich': + return RichViewer; + default: + return SimpleViewer; + } + }, + viewerError() { + return this.activeViewer.renderError; + }, + }, +}; +</script> +<template> + <div class="blob-viewer" :data-type="activeViewer.type"> + <gl-loading-icon v-if="loading" size="md" color="dark" class="my-4 mx-auto" /> + + <template v-else> + <blob-content-error v-if="viewerError" :viewer-error="viewerError" /> + <component :is="viewer" v-else ref="contentViewer" :content="content" /> + </template> + </div> +</template> diff --git a/app/assets/javascripts/blob/components/blob_content_error.vue b/app/assets/javascripts/blob/components/blob_content_error.vue new file mode 100644 index 00000000000..0f1af0a962d --- /dev/null +++ b/app/assets/javascripts/blob/components/blob_content_error.vue @@ -0,0 +1,15 @@ +<script> +export default { + props: { + viewerError: { + type: String, + required: true, + }, + }, +}; +</script> +<template> + <div class="file-content code"> + <div class="text-center py-4" v-html="viewerError"></div> + </div> +</template> diff --git a/app/assets/javascripts/blob/components/blob_header_default_actions.vue b/app/assets/javascripts/blob/components/blob_header_default_actions.vue index f5157fba819..6b38b871606 100644 --- a/app/assets/javascripts/blob/components/blob_header_default_actions.vue +++ b/app/assets/javascripts/blob/components/blob_header_default_actions.vue @@ -36,11 +36,6 @@ export default { return this.activeViewer === RICH_BLOB_VIEWER; }, }, - methods: { - requestCopyContents() { - this.$emit('copy'); - }, - }, BTN_COPY_CONTENTS_TITLE, BTN_DOWNLOAD_TITLE, BTN_RAW_TITLE, @@ -53,7 +48,7 @@ export default { :aria-label="$options.BTN_COPY_CONTENTS_TITLE" :title="$options.BTN_COPY_CONTENTS_TITLE" :disabled="copyDisabled" - @click="requestCopyContents" + data-clipboard-target="#blob-code-content" > <gl-icon name="copy-to-clipboard" :size="14" /> </gl-button> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 43f5e9954ce..6d2b11e39d3 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import Vue from 'vue'; import Cookies from 'js-cookie'; -import { GlEmptyState } from '@gitlab/ui'; +import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; import filterMixins from 'ee_else_ce/analytics/cycle_analytics/mixins/filter_mixins'; import Flash from '../flash'; import { __ } from '~/locale'; @@ -28,6 +28,7 @@ export default () => { name: 'CycleAnalytics', components: { GlEmptyState, + GlLoadingIcon, banner, 'stage-issue-component': stageComponent, 'stage-plan-component': stageComponent, diff --git a/app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql index 64c894df115..b202ed12f80 100644 --- a/app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql +++ b/app/assets/javascripts/graphql_shared/fragments/blobviewer.fragment.graphql @@ -1,6 +1,7 @@ fragment BlobViewer on SnippetBlobViewer { collapsed - loadingPartialName renderError tooLarge + type + fileType } diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue index 49e0ef35cb8..4703a940e08 100644 --- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue +++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue @@ -2,13 +2,19 @@ import BlobEmbeddable from '~/blob/components/blob_embeddable.vue'; import { SNIPPET_VISIBILITY_PUBLIC } from '../constants'; import BlobHeader from '~/blob/components/blob_header.vue'; -import GetSnippetBlobQuery from '../queries/snippet.blob.query.graphql'; +import BlobContent from '~/blob/components/blob_content.vue'; import { GlLoadingIcon } from '@gitlab/ui'; +import GetSnippetBlobQuery from '../queries/snippet.blob.query.graphql'; +import GetBlobContent from '../queries/snippet.blob.content.query.graphql'; + +import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants'; + export default { components: { BlobEmbeddable, BlobHeader, + BlobContent, GlLoadingIcon, }, apollo: { @@ -20,6 +26,23 @@ export default { }; }, update: data => data.snippets.edges[0].node.blob, + result(res) { + const viewer = res.data.snippets.edges[0].node.blob.richViewer + ? RICH_BLOB_VIEWER + : SIMPLE_BLOB_VIEWER; + this.switchViewer(viewer, true); + }, + }, + blobContent: { + query: GetBlobContent, + variables() { + return { + ids: this.snippet.id, + rich: this.activeViewerType === RICH_BLOB_VIEWER, + }; + }, + update: data => + data.snippets.edges[0].node.blob.richData || data.snippets.edges[0].node.blob.plainData, }, }, props: { @@ -31,6 +54,8 @@ export default { data() { return { blob: {}, + blobContent: '', + activeViewerType: window.location.hash ? SIMPLE_BLOB_VIEWER : '', }; }, computed: { @@ -40,6 +65,18 @@ export default { isBlobLoading() { return this.$apollo.queries.blob.loading; }, + isContentLoading() { + return this.$apollo.queries.blobContent.loading; + }, + viewer() { + const { richViewer, simpleViewer } = this.blob; + return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer; + }, + }, + methods: { + switchViewer(newViewer, respectHash = false) { + this.activeViewerType = respectHash && window.location.hash ? SIMPLE_BLOB_VIEWER : newViewer; + }, }, }; </script> @@ -49,11 +86,12 @@ export default { <gl-loading-icon v-if="isBlobLoading" :label="__('Loading blob')" - :size="2" + size="lg" class="prepend-top-20 append-bottom-20" /> <article v-else class="file-holder snippet-file-content"> - <blob-header :blob="blob" /> + <blob-header :blob="blob" :active-viewer-type="viewer.type" @viewer-changed="switchViewer" /> + <blob-content :loading="isContentLoading" :content="blobContent" :active-viewer="viewer" /> </article> </div> </template> diff --git a/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql b/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql new file mode 100644 index 00000000000..889a88dd93c --- /dev/null +++ b/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql @@ -0,0 +1,13 @@ +query SnippetBlobContent($ids: [ID!], $rich: Boolean!) { + snippets(ids: $ids) { + edges { + node { + id + blob { + richData @include(if: $rich) + plainData @skip(if: $rich) + } + } + } + } +} diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js new file mode 100644 index 00000000000..d4c1808eec2 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/constants.js @@ -0,0 +1,3 @@ +export const HIGHLIGHT_CLASS_NAME = 'hll'; + +export default {}; diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/index.js b/app/assets/javascripts/vue_shared/components/blob_viewers/index.js new file mode 100644 index 00000000000..72fba9392f9 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/index.js @@ -0,0 +1,4 @@ +import RichViewer from './rich_viewer.vue'; +import SimpleViewer from './simple_viewer.vue'; + +export { RichViewer, SimpleViewer }; diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js new file mode 100644 index 00000000000..582213ee8d3 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js @@ -0,0 +1,8 @@ +export default { + props: { + content: { + type: String, + required: true, + }, + }, +}; diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue new file mode 100644 index 00000000000..b3a1df8f303 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue @@ -0,0 +1,10 @@ +<script> +import ViewerMixin from './mixins'; + +export default { + mixins: [ViewerMixin], +}; +</script> +<template> + <div v-html="content"></div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue new file mode 100644 index 00000000000..e64c7132117 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue @@ -0,0 +1,68 @@ +<script> +import ViewerMixin from './mixins'; +import { GlIcon } from '@gitlab/ui'; +import { HIGHLIGHT_CLASS_NAME } from './constants'; + +export default { + components: { + GlIcon, + }, + mixins: [ViewerMixin], + data() { + return { + highlightedLine: null, + }; + }, + computed: { + lineNumbers() { + return this.content.split('\n').length; + }, + }, + mounted() { + const { hash } = window.location; + if (hash) this.scrollToLine(hash, true); + }, + methods: { + scrollToLine(hash, scroll = false) { + const lineToHighlight = hash && this.$el.querySelector(hash); + const currentlyHighlighted = this.highlightedLine; + if (lineToHighlight) { + if (currentlyHighlighted) { + currentlyHighlighted.classList.remove(HIGHLIGHT_CLASS_NAME); + } + + lineToHighlight.classList.add(HIGHLIGHT_CLASS_NAME); + this.highlightedLine = lineToHighlight; + if (scroll) { + lineToHighlight.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + } + }, + }, + userColorScheme: window.gon.user_color_scheme, +}; +</script> +<template> + <div + class="file-content code js-syntax-highlight qa-file-content" + :class="$options.userColorScheme" + > + <div class="line-numbers"> + <a + v-for="line in lineNumbers" + :id="`L${line}`" + :key="line" + class="diff-line-num js-line-number" + :href="`#LC${line}`" + :data-line-number="line" + @click="scrollToLine(`#LC${line}`)" + > + <gl-icon :size="12" name="link" /> + {{ line }} + </a> + </div> + <div class="blob-content"> + <pre class="code highlight"><code id="blob-code-content" v-html="content"></code></pre> + </div> + </div> +</template> diff --git a/app/assets/stylesheets/framework/highlight.scss b/app/assets/stylesheets/framework/highlight.scss index ee6e53adaf7..73a2170fc68 100644 --- a/app/assets/stylesheets/framework/highlight.scss +++ b/app/assets/stylesheets/framework/highlight.scss @@ -30,7 +30,6 @@ .line { display: block; width: 100%; - min-height: 1.5em; padding-left: 10px; padding-right: 10px; white-space: pre; @@ -48,10 +47,10 @@ font-family: $monospace-font; display: block; font-size: $code-font-size !important; - min-height: 1.5em; white-space: nowrap; - i { + i, + svg { float: left; margin-top: 3px; margin-right: 5px; @@ -62,12 +61,20 @@ &:focus { outline: none; - i { + i, + svg { visibility: visible; } } } } + + pre .line, + .line-numbers a { + font-size: 0.8125rem; + line-height: 1.1875rem; + min-height: 1.1875rem; + } } // Vertically aligns <table> line numbers (eg. blame view) diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss index 76cd4f34865..21b23feb57f 100644 --- a/app/assets/stylesheets/pages/cycle_analytics.scss +++ b/app/assets/stylesheets/pages/cycle_analytics.scss @@ -109,14 +109,6 @@ top: $gl-padding-top; } - .fa-spinner { - font-size: 28px; - position: relative; - margin-left: -20px; - left: 50%; - margin-top: 36px; - } - .stage-panel-body { display: flex; flex-wrap: wrap; diff --git a/app/controllers/concerns/metrics_dashboard.rb b/app/controllers/concerns/metrics_dashboard.rb index dc392147cb8..fa79f3bc4e6 100644 --- a/app/controllers/concerns/metrics_dashboard.rb +++ b/app/controllers/concerns/metrics_dashboard.rb @@ -5,6 +5,7 @@ module MetricsDashboard include RenderServiceResults include ChecksCollaboration + include EnvironmentsHelper extend ActiveSupport::Concern @@ -15,8 +16,9 @@ module MetricsDashboard metrics_dashboard_params.to_h.symbolize_keys ) - if include_all_dashboards? && result - result[:all_dashboards] = all_dashboards + if result + result[:all_dashboards] = all_dashboards if include_all_dashboards? + result[:metrics_data] = metrics_data(project_for_dashboard, environment_for_dashboard) if project_for_dashboard && environment_for_dashboard end respond_to do |format| @@ -76,10 +78,14 @@ module MetricsDashboard defined?(project) ? project : nil end + def environment_for_dashboard + defined?(environment) ? environment : nil + end + def dashboard_success_response(result) { status: :ok, - json: result.slice(:all_dashboards, :dashboard, :status) + json: result.slice(:all_dashboards, :dashboard, :status, :metrics_data) } end diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index 579a57a27de..fd330d4efd9 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true module EnvironmentsHelper + include ActionView::Helpers::AssetUrlHelper prepend_if_ee('::EE::EnvironmentsHelper') # rubocop: disable Cop/InjectEnterpriseEditionModule def environments_list_data @@ -21,7 +22,7 @@ module EnvironmentsHelper { "settings-path" => edit_project_service_path(project, 'prometheus'), "clusters-path" => project_clusters_path(project), - "current-environment-name": environment.name, + "current-environment-name" => environment.name, "documentation-path" => help_page_path('administration/monitoring/prometheus/index.md'), "empty-getting-started-svg-path" => image_path('illustrations/monitoring/getting_started.svg'), "empty-loading-svg-path" => image_path('illustrations/monitoring/loading.svg'), diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 70409774a50..b0d9dfb0d37 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -5,7 +5,7 @@ %banner{ "v-if" => "!isOverviewDialogDismissed", "documentation-link": help_page_path('user/analytics/value_stream_analytics.md'), "v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" } - = icon("spinner spin", "v-show" => "isLoading") + %gl-loading-icon{ "v-show" => "isLoading", "size" => "lg" } .wrapper{ "v-show" => "!isLoading && !hasError" } .card .card-header @@ -57,8 +57,7 @@ %ul %stage-nav-item{ "v-for" => "stage in state.stages", ":key" => '`ca-stage-title-${stage.title}`', '@select' => 'selectStage(stage)', ":title" => "stage.title", ":is-user-allowed" => "stage.isUserAllowed", ":value" => "stage.value", ":is-active" => "stage.active" } .section.stage-events - %template{ "v-if" => "isLoadingStage" } - = icon("spinner spin") + %gl-loading-icon{ "v-show" => "isLoadingStage", "size" => "lg" } %template{ "v-if" => "currentStage && !currentStage.isUserAllowed" } = render partial: "no_access" %template{ "v-else" => true } diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml index 768e4422206..422a467574b 100644 --- a/app/views/projects/snippets/show.html.haml +++ b/app/views/projects/snippets/show.html.haml @@ -12,7 +12,7 @@ %article.file-holder.snippet-file-content = render 'shared/snippets/blob' - .row-content-block.top-block.content-component-block - = render 'award_emoji/awards_block', awardable: @snippet, inline: true +.row-content-block.top-block.content-component-block + = render 'award_emoji/awards_block', awardable: @snippet, inline: true - #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true +#notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 080c0ab6ece..30f760f2122 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -13,7 +13,7 @@ %article.file-holder.snippet-file-content = render 'shared/snippets/blob' - .row-content-block.top-block.content-component-block - = render 'award_emoji/awards_block', awardable: @snippet, inline: true +.row-content-block.top-block.content-component-block + = render 'award_emoji/awards_block', awardable: @snippet, inline: true - #notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => false +#notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => false diff --git a/changelogs/unreleased/196184-convert-insights-to-echarts.yml b/changelogs/unreleased/196184-convert-insights-to-echarts.yml new file mode 100644 index 00000000000..87219b3ff52 --- /dev/null +++ b/changelogs/unreleased/196184-convert-insights-to-echarts.yml @@ -0,0 +1,5 @@ +--- +title: Move insights charts to echarts +merge_request: 24661 +author: +type: other diff --git a/changelogs/unreleased/196797-blob-content.yml b/changelogs/unreleased/196797-blob-content.yml new file mode 100644 index 00000000000..b1443f60f62 --- /dev/null +++ b/changelogs/unreleased/196797-blob-content.yml @@ -0,0 +1,5 @@ +--- +title: Refactored snippets view to Vue +merge_request: 25188 +author: +type: other diff --git a/changelogs/unreleased/204740-migrate-fa-spinner-to-spinner-within-app-views-projects-cycle_anal.yml b/changelogs/unreleased/204740-migrate-fa-spinner-to-spinner-within-app-views-projects-cycle_anal.yml new file mode 100644 index 00000000000..e417b770668 --- /dev/null +++ b/changelogs/unreleased/204740-migrate-fa-spinner-to-spinner-within-app-views-projects-cycle_anal.yml @@ -0,0 +1,5 @@ +--- +title: Update loading icon in Value Stream Analytics view +merge_request: 24861 +author: +type: other diff --git a/changelogs/unreleased/defect-safari-line-numbers-height.yml b/changelogs/unreleased/defect-safari-line-numbers-height.yml new file mode 100644 index 00000000000..e72a7ffc107 --- /dev/null +++ b/changelogs/unreleased/defect-safari-line-numbers-height.yml @@ -0,0 +1,5 @@ +--- +title: Fix code line and line number alignment in Safari +merge_request: 24820 +author: +type: fixed diff --git a/doc/api/epics.md b/doc/api/epics.md index 078bdfdba69..b8eb1ab9f9a 100644 --- a/doc/api/epics.md +++ b/doc/api/epics.md @@ -1,4 +1,4 @@ -# Epics API **(ULTIMATE)** +# Epics API **(PREMIUM)** Every API call to epic must be authenticated. diff --git a/doc/ci/review_apps/img/enable_review_app_v12_8.png b/doc/ci/review_apps/img/enable_review_app_v12_8.png Binary files differnew file mode 100644 index 00000000000..364fe402787 --- /dev/null +++ b/doc/ci/review_apps/img/enable_review_app_v12_8.png diff --git a/doc/ci/review_apps/index.md b/doc/ci/review_apps/index.md index 23cb0362867..cc081f2543a 100644 --- a/doc/ci/review_apps/index.md +++ b/doc/ci/review_apps/index.md @@ -55,9 +55,31 @@ The process of configuring Review Apps is as follows: 1. Set up the infrastructure to host and deploy the Review Apps (check the [examples](#review-apps-examples) below). 1. [Install](https://docs.gitlab.com/runner/install/) and [configure](https://docs.gitlab.com/runner/commands/) a Runner to do deployment. -1. Set up a job in `.gitlab-ci.yml` that uses the [predefined CI environment variable](../variables/README.md) `${CI_COMMIT_REF_NAME}` to create dynamic environments and restrict it to run only on branches. +1. Set up a job in `.gitlab-ci.yml` that uses the [predefined CI environment variable](../variables/README.md) `${CI_COMMIT_REF_NAME}` + to create dynamic environments and restrict it to run only on branches. + Alternatively, you can get a YML template for this job by [enabling review apps](#enable-review-apps-button) for your project. 1. Optionally, set a job that [manually stops](../environments.md#stopping-an-environment) the Review Apps. +### Enable Review Apps button + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/118844) in GitLab 12.8. + +When configuring Review Apps for a project, you need to add a new job to `.gitlab-ci.yml`, +as mentioned above. To facilitate this and if you are using Kubernetes, you can click +the **Enable Review Apps** button and GitLab will prompt you with a template code block that +you can copy and paste into `.gitlab-ci.yml` as a starting point. To do so: + +1. Go to the project your want to create a Review App job for. +1. From the left nav, go to **Operations** > **Environments**. +1. Click on the **Enable Review Apps** button. It is available to you + if you have Developer or higher [permissions](../../user/permissions.md) to that project. +1. Copy the provided code snippet and paste it into your + `.gitlab-ci.yml` file: + + ![Enable Review Apps modal](img/enable_review_app_v12_8.png) + +1. Feel free to tune this template to your own needs. + ## Review Apps examples The following are example projects that demonstrate Review App configuration: diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 4f42afe4e79..8cf60342446 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -93,6 +93,7 @@ The following table depicts the various user permission levels in a project. | Manage/Accept merge requests | | | ✓ | ✓ | ✓ | | Create new environments | | | ✓ | ✓ | ✓ | | Stop environments | | | ✓ | ✓ | ✓ | +| Enable Review Apps | | | ✓ | ✓ | ✓ | | Add tags | | | ✓ | ✓ | ✓ | | Cancel and retry jobs | | | ✓ | ✓ | ✓ | | Create or update commit status | | | ✓ (*5*) | ✓ | ✓ | diff --git a/doc/user/project/insights/img/insights_example_pie_chart.png b/doc/user/project/insights/img/insights_example_pie_chart.png Binary files differdeleted file mode 100644 index 3480bce6738..00000000000 --- a/doc/user/project/insights/img/insights_example_pie_chart.png +++ /dev/null diff --git a/doc/user/project/insights/index.md b/doc/user/project/insights/index.md index ec3831f2d27..3fb661f9349 100644 --- a/doc/user/project/insights/index.md +++ b/doc/user/project/insights/index.md @@ -96,7 +96,7 @@ The following table lists available parameters for charts: | Keyword | Description | |:---------------------------------------------------|:------------| | [`title`](#title) | The title of the chart. This will displayed on the Insights page. | -| [`type`](#type) | The type of chart: `bar`, `line`, `stacked-bar`, `pie` etc. | +| [`type`](#type) | The type of chart: `bar`, `line` or `stacked-bar`. | | [`query`](#query) | A hash that defines the conditions for issues / merge requests to be part of the chart. | ## Parameter details @@ -132,7 +132,6 @@ Supported values are: | ----- | ------- | | `bar` | ![Insights example bar chart](img/insights_example_bar_chart.png) | | `bar` (time series, i.e. when `group_by` is used) | ![Insights example bar time series chart](img/insights_example_bar_time_series_chart.png) | -| `pie` | ![Insights example pie chart](img/insights_example_pie_chart.png) | | `line` | ![Insights example stacked bar chart](img/insights_example_line_chart.png) | | `stacked-bar` | ![Insights example stacked bar chart](img/insights_example_stacked_bar_chart.png) | diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb deleted file mode 100644 index c813484347e..00000000000 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module QA - context 'Verify' do - describe 'CI variable support' do - it 'user adds a CI variable', :smoke do - Flow::Login.sign_in - - project = Resource::Project.fabricate_via_api! do |project| - project.name = 'project-with-ci-variables' - project.description = 'project with CI variables' - end - - Resource::CiVariable.fabricate_via_api! do |resource| - resource.project = project - resource.key = 'VARIABLE_KEY' - resource.value = 'some_CI_variable' - resource.masked = false - end - - project.visit! - - Page::Project::Menu.perform(&:go_to_ci_cd_settings) - - Page::Project::Settings::CICD.perform do |settings| - settings.expand_ci_variables do |page| - expect(page).to have_field(with: 'VARIABLE_KEY') - expect(page).not_to have_field(with: 'some_CI_variable') - - page.reveal_variables - - expect(page).to have_field(with: 'some_CI_variable') - end - end - end - end - end -end diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb new file mode 100644 index 00000000000..9dad5ad8fb5 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module QA + context 'Verify' do + describe 'Add or Remove CI variable via UI', :smoke do + let!(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'project-with-ci-variables' + project.description = 'project with CI variables' + end + end + + before do + Flow::Login.sign_in + add_ci_variable + open_ci_cd_settings + end + + it 'user adds a CI variable' do + Page::Project::Settings::CICD.perform do |settings| + settings.expand_ci_variables do |page| + expect(page).to have_field(with: 'VARIABLE_KEY') + expect(page).not_to have_field(with: 'some_CI_variable') + + page.reveal_variables + + expect(page).to have_field(with: 'some_CI_variable') + end + end + end + + it 'user removes a CI variable' do + Page::Project::Settings::CICD.perform do |settings| + settings.expand_ci_variables do |page| + page.remove_variable + + expect(page).not_to have_field(with: 'VARIABLE_KEY') + end + end + end + + private + + def add_ci_variable + Resource::CiVariable.fabricate_via_browser_ui! do |ci_variable| + ci_variable.project = project + ci_variable.key = 'VARIABLE_KEY' + ci_variable.value = 'some_CI_variable' + ci_variable.masked = false + end + end + + def open_ci_cd_settings + project.visit! + Page::Project::Menu.perform(&:go_to_ci_cd_settings) + end + end + end +end diff --git a/qa/qa/support/repeater.rb b/qa/qa/support/repeater.rb index 53d72f2f410..14771243beb 100644 --- a/qa/qa/support/repeater.rb +++ b/qa/qa/support/repeater.rb @@ -10,13 +10,13 @@ module QA RetriesExceededError = Class.new(RuntimeError) WaitExceededError = Class.new(RuntimeError) - def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false) + def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false, log: true) attempts = 0 start = Time.now begin while remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration) - QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") if max_attempts + QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") if max_attempts && log result = yield return result if result diff --git a/qa/qa/support/wait_for_requests.rb b/qa/qa/support/wait_for_requests.rb index 5d5ba70a0c2..c58882a11ea 100644 --- a/qa/qa/support/wait_for_requests.rb +++ b/qa/qa/support/wait_for_requests.rb @@ -6,7 +6,7 @@ module QA module_function def wait_for_requests - Waiter.wait_until do + Waiter.wait_until(log: false) do finished_all_ajax_requests? && finished_all_axios_requests? end end diff --git a/qa/qa/support/waiter.rb b/qa/qa/support/waiter.rb index 18ccee80074..9ccc0d9484f 100644 --- a/qa/qa/support/waiter.rb +++ b/qa/qa/support/waiter.rb @@ -7,15 +7,17 @@ module QA module_function - def wait_until(max_duration: singleton_class::DEFAULT_MAX_WAIT_TIME, reload_page: nil, sleep_interval: 0.1, raise_on_failure: true, retry_on_exception: false) - QA::Runtime::Logger.debug( - <<~MSG.tr("\n", ' ') - with wait_until: max_duration: #{max_duration}; - reload_page: #{reload_page}; - sleep_interval: #{sleep_interval}; - raise_on_failure: #{raise_on_failure} - MSG - ) + def wait_until(max_duration: singleton_class::DEFAULT_MAX_WAIT_TIME, reload_page: nil, sleep_interval: 0.1, raise_on_failure: true, retry_on_exception: false, log: true) + if log + QA::Runtime::Logger.debug( + <<~MSG.tr("\n", ' ') + with wait_until: max_duration: #{max_duration}; + reload_page: #{reload_page}; + sleep_interval: #{sleep_interval}; + raise_on_failure: #{raise_on_failure} + MSG + ) + end result = nil self.repeat_until( @@ -23,11 +25,12 @@ module QA reload_page: reload_page, sleep_interval: sleep_interval, raise_on_failure: raise_on_failure, - retry_on_exception: retry_on_exception + retry_on_exception: retry_on_exception, + log: log ) do result = yield end - QA::Runtime::Logger.debug("ended wait_until") + QA::Runtime::Logger.debug("ended wait_until") if log result end diff --git a/qa/spec/support/repeater_spec.rb b/qa/spec/support/repeater_spec.rb index 20dca6608f6..b5d5058ef49 100644 --- a/qa/spec/support/repeater_spec.rb +++ b/qa/spec/support/repeater_spec.rb @@ -381,5 +381,35 @@ describe QA::Support::Repeater do end end end + + it 'logs attempts' do + attempted = false + + expect do + subject.repeat_until(max_attempts: 1) do + unless attempted + attempted = true + break false + end + + true + end + end.to output(/Attempt number/).to_stdout_from_any_process + end + + it 'allows logging to be silenced' do + attempted = false + + expect do + subject.repeat_until(max_attempts: 1, log: false) do + unless attempted + attempted = true + break false + end + + true + end + end.not_to output.to_stdout_from_any_process + end end end diff --git a/qa/spec/support/waiter_spec.rb b/qa/spec/support/waiter_spec.rb index 40abf0d72a2..35f1e01289a 100644 --- a/qa/spec/support/waiter_spec.rb +++ b/qa/spec/support/waiter_spec.rb @@ -34,6 +34,11 @@ describe QA::Support::Waiter do end end + it 'allows logs to be silenced' do + expect { subject.wait_until(max_duration: 0, raise_on_failure: false, log: false) { false } } + .not_to output.to_stdout_from_any_process + end + it 'sets max_duration to 60 by default' do expect(subject).to receive(:repeat_until).with(hash_including(max_duration: 60)) diff --git a/spec/controllers/concerns/metrics_dashboard_spec.rb b/spec/controllers/concerns/metrics_dashboard_spec.rb index 466021d6ecd..4e42171e3d3 100644 --- a/spec/controllers/concerns/metrics_dashboard_spec.rb +++ b/spec/controllers/concerns/metrics_dashboard_spec.rb @@ -45,6 +45,7 @@ describe MetricsDashboard do it 'returns the specified dashboard' do expect(json_response['dashboard']['dashboard']).to eq('Environment metrics') expect(json_response).not_to have_key('all_dashboards') + expect(json_response).not_to have_key('metrics_data') end context 'when the params are in an alternate format' do @@ -53,6 +54,25 @@ describe MetricsDashboard do it 'returns the specified dashboard' do expect(json_response['dashboard']['dashboard']).to eq('Environment metrics') expect(json_response).not_to have_key('all_dashboards') + expect(json_response).not_to have_key('metrics_data') + end + end + + context 'when environment for dashboard is available' do + let(:params) { { environment: environment } } + + before do + allow(controller).to receive(:project).and_return(project) + allow(controller).to receive(:environment).and_return(environment) + allow(controller) + .to receive(:metrics_dashboard_params) + .and_return(params) + end + + it 'returns the specified dashboard' do + expect(json_response['dashboard']['dashboard']).to eq('Environment metrics') + expect(json_response).not_to have_key('all_dashboards') + expect(json_response).to have_key('metrics_data') end end diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 7f0f8e36564..6c63b220322 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -489,7 +489,7 @@ describe Projects::EnvironmentsController do end shared_examples_for '200 response' do - let(:expected_keys) { %w(dashboard status) } + let(:expected_keys) { %w(dashboard status metrics_data) } it_behaves_like 'correctly formatted response', :ok end diff --git a/spec/frontend/blob/components/blob_content_error_spec.js b/spec/frontend/blob/components/blob_content_error_spec.js new file mode 100644 index 00000000000..58a9ee761df --- /dev/null +++ b/spec/frontend/blob/components/blob_content_error_spec.js @@ -0,0 +1,27 @@ +import { shallowMount } from '@vue/test-utils'; +import BlobContentError from '~/blob/components/blob_content_error.vue'; + +describe('Blob Content Error component', () => { + let wrapper; + const viewerError = '<h1 id="error">Foo Error</h1>'; + + function createComponent() { + wrapper = shallowMount(BlobContentError, { + propsData: { + viewerError, + }, + }); + } + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the passed error without transformations', () => { + expect(wrapper.html()).toContain(viewerError); + }); +}); diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js new file mode 100644 index 00000000000..6a130c9c43d --- /dev/null +++ b/spec/frontend/blob/components/blob_content_spec.js @@ -0,0 +1,70 @@ +import { shallowMount } from '@vue/test-utils'; +import BlobContent from '~/blob/components/blob_content.vue'; +import BlobContentError from '~/blob/components/blob_content_error.vue'; +import { + RichViewerMock, + SimpleViewerMock, + RichBlobContentMock, + SimpleBlobContentMock, +} from './mock_data'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers'; + +describe('Blob Content component', () => { + let wrapper; + + function createComponent(propsData = {}, activeViewer = SimpleViewerMock) { + wrapper = shallowMount(BlobContent, { + propsData: { + loading: false, + activeViewer, + ...propsData, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + describe('rendering', () => { + it('renders loader if `loading: true`', () => { + createComponent({ loading: true }); + expect(wrapper.contains(GlLoadingIcon)).toBe(true); + expect(wrapper.contains(BlobContentError)).toBe(false); + expect(wrapper.contains(RichViewer)).toBe(false); + expect(wrapper.contains(SimpleViewer)).toBe(false); + }); + + it('renders error if there is any in the viewer', () => { + const renderError = 'Oops'; + const viewer = Object.assign({}, SimpleViewerMock, { renderError }); + createComponent({}, viewer); + expect(wrapper.contains(GlLoadingIcon)).toBe(false); + expect(wrapper.contains(BlobContentError)).toBe(true); + expect(wrapper.contains(RichViewer)).toBe(false); + expect(wrapper.contains(SimpleViewer)).toBe(false); + }); + + it.each` + type | mock | viewer + ${'simple'} | ${SimpleViewerMock} | ${SimpleViewer} + ${'rich'} | ${RichViewerMock} | ${RichViewer} + `( + 'renders $type viewer when activeViewer is $type and no loading or error detected', + ({ mock, viewer }) => { + createComponent({}, mock); + expect(wrapper.contains(viewer)).toBe(true); + }, + ); + + it.each` + content | mock | viewer + ${SimpleBlobContentMock.plainData} | ${SimpleViewerMock} | ${SimpleViewer} + ${RichBlobContentMock.richData} | ${RichViewerMock} | ${RichViewer} + `('renders correct content that is passed to the component', ({ content, mock, viewer }) => { + createComponent({ content }, mock); + expect(wrapper.find(viewer).html()).toContain(content); + }); + }); +}); diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js index 5da0d40ab14..39d627e71c5 100644 --- a/spec/frontend/blob/components/blob_header_default_actions_spec.js +++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js @@ -67,13 +67,4 @@ describe('Blob Header Default Actions', () => { expect(buttons.at(0).attributes('disabled')).toBeTruthy(); }); }); - - describe('functionally', () => { - it('emits an event when a Copy Contents button is clicked', () => { - jest.spyOn(wrapper.vm, '$emit'); - buttons.at(0).vm.$emit('click'); - - expect(wrapper.vm.$emit).toHaveBeenCalledWith('copy'); - }); - }); }); diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js index 4f7b297aba0..bfcca14324f 100644 --- a/spec/frontend/blob/components/mock_data.js +++ b/spec/frontend/blob/components/mock_data.js @@ -1,29 +1,43 @@ +import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants'; + +export const SimpleViewerMock = { + collapsed: false, + loadingPartialName: 'loading', + renderError: null, + tooLarge: false, + type: SIMPLE_BLOB_VIEWER, + fileType: 'text', +}; + +export const RichViewerMock = { + collapsed: false, + loadingPartialName: 'loading', + renderError: null, + tooLarge: false, + type: RICH_BLOB_VIEWER, + fileType: 'markdown', +}; + export const Blob = { binary: false, - highlightedData: - '<h1 data-sourcepos="1:1-1:19" dir="auto">\n<a id="user-content-this-one-is-dummy" class="anchor" href="#this-one-is-dummy" aria-hidden="true"></a>This one is dummy</h1>\n<h2 data-sourcepos="3:1-3:21" dir="auto">\n<a id="user-content-and-has-sub-header" class="anchor" href="#and-has-sub-header" aria-hidden="true"></a>And has sub-header</h2>\n<p data-sourcepos="5:1-5:27" dir="auto">Even some stupid text here</p>', name: 'dummy.md', path: 'dummy.md', rawPath: '/flightjs/flight/snippets/51/raw', size: 75, simpleViewer: { - collapsed: false, - fileType: 'text', - loadAsync: true, - loadingPartialName: 'loading', - renderError: null, - tooLarge: false, - type: 'simple', + ...SimpleViewerMock, }, richViewer: { - collapsed: false, - fileType: 'markup', - loadAsync: true, - loadingPartialName: 'loading', - renderError: null, - tooLarge: false, - type: 'rich', + ...RichViewerMock, }, }; +export const RichBlobContentMock = { + richData: '<h1>Rich</h1>', +}; + +export const SimpleBlobContentMock = { + plainData: 'Plain', +}; + export default {}; diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js index efc1c6dcef9..c4f1dd0ca35 100644 --- a/spec/frontend/snippets/components/snippet_blob_view_spec.js +++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js @@ -1,14 +1,18 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import { GlLoadingIcon } from '@gitlab/ui'; import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue'; import BlobHeader from '~/blob/components/blob_header.vue'; import BlobEmbeddable from '~/blob/components/blob_embeddable.vue'; +import BlobContent from '~/blob/components/blob_content.vue'; +import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers'; import { SNIPPET_VISIBILITY_PRIVATE, SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PUBLIC, } from '~/snippets/constants'; +import { Blob as BlobMock, SimpleViewerMock, RichViewerMock } from 'jest/blob/components/mock_data'; + describe('Blob Embeddable', () => { let wrapper; const snippet = { @@ -16,27 +20,42 @@ describe('Blob Embeddable', () => { webUrl: 'https://foo.bar', visibilityLevel: SNIPPET_VISIBILITY_PUBLIC, }; + const dataMock = { + blob: BlobMock, + activeViewerType: SimpleViewerMock.type, + }; - function createComponent(props = {}, loading = false) { + function createComponent( + props = {}, + data = dataMock, + blobLoading = false, + contentLoading = false, + ) { const $apollo = { queries: { blob: { - loading, + loading: blobLoading, + }, + blobContent: { + loading: contentLoading, }, }, }; - wrapper = shallowMount(SnippetBlobView, { + wrapper = mount(SnippetBlobView, { propsData: { snippet: { ...snippet, ...props, }, }, + data() { + return { + ...data, + }; + }, mocks: { $apollo }, }); - - wrapper.vm.$apollo.queries.blob.loading = false; } afterEach(() => { @@ -48,6 +67,7 @@ describe('Blob Embeddable', () => { createComponent(); expect(wrapper.find(BlobEmbeddable).exists()).toBe(true); expect(wrapper.find(BlobHeader).exists()).toBe(true); + expect(wrapper.find(BlobContent).exists()).toBe(true); }); it.each([SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PRIVATE, 'foo'])( @@ -68,9 +88,92 @@ describe('Blob Embeddable', () => { }); it('shows loading icon while blob data is in flight', () => { - createComponent({}, true); + createComponent({}, dataMock, true); expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); expect(wrapper.find('.snippet-file-content').exists()).toBe(false); }); + + it('sets simple viewer correctly', () => { + createComponent(); + expect(wrapper.find(SimpleViewer).exists()).toBe(true); + }); + + it('sets rich viewer correctly', () => { + const data = Object.assign({}, dataMock, { + activeViewerType: RichViewerMock.type, + }); + createComponent({}, data); + expect(wrapper.find(RichViewer).exists()).toBe(true); + }); + + it('correctly switches viewer type', () => { + createComponent(); + expect(wrapper.find(SimpleViewer).exists()).toBe(true); + + wrapper.vm.switchViewer(RichViewerMock.type); + + return wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.find(RichViewer).exists()).toBe(true); + wrapper.vm.switchViewer(SimpleViewerMock.type); + }) + .then(() => { + expect(wrapper.find(SimpleViewer).exists()).toBe(true); + }); + }); + + describe('URLS with hash', () => { + beforeEach(() => { + window.location.hash = '#LC2'; + }); + + afterEach(() => { + window.location.hash = ''; + }); + + it('renders simple viewer by default if URL contains hash', () => { + createComponent(); + + expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type); + expect(wrapper.find(SimpleViewer).exists()).toBe(true); + }); + + describe('switchViewer()', () => { + it('by default switches to the passed viewer', () => { + createComponent(); + + wrapper.vm.switchViewer(RichViewerMock.type); + return wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.vm.activeViewerType).toBe(RichViewerMock.type); + expect(wrapper.find(RichViewer).exists()).toBe(true); + + wrapper.vm.switchViewer(SimpleViewerMock.type); + }) + .then(() => { + expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type); + expect(wrapper.find(SimpleViewer).exists()).toBe(true); + }); + }); + + it('respects hash over richViewer in the blob when corresponding parameter is passed', () => { + createComponent( + {}, + { + blob: BlobMock, + }, + ); + expect(wrapper.vm.blob.richViewer).toEqual(expect.any(Object)); + + wrapper.vm.switchViewer(RichViewerMock.type, true); + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.activeViewerType).toBe(SimpleViewerMock.type); + expect(wrapper.find(SimpleViewer).exists()).toBe(true); + }); + }); + }); + }); }); }); diff --git a/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap new file mode 100644 index 00000000000..87f2a8f9eff --- /dev/null +++ b/spec/frontend/vue_shared/components/blob_viewers/__snapshots__/simple_viewer_spec.js.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Blob Simple Viewer component rendering matches the snapshot 1`] = ` +<div + class="file-content code js-syntax-highlight qa-file-content" +> + <div + class="line-numbers" + > + <a + class="diff-line-num js-line-number" + data-line-number="1" + href="#LC1" + id="L1" + > + <gl-icon-stub + name="link" + size="12" + /> + + 1 + + </a> + <a + class="diff-line-num js-line-number" + data-line-number="2" + href="#LC2" + id="L2" + > + <gl-icon-stub + name="link" + size="12" + /> + + 2 + + </a> + <a + class="diff-line-num js-line-number" + data-line-number="3" + href="#LC3" + id="L3" + > + <gl-icon-stub + name="link" + size="12" + /> + + 3 + + </a> + </div> + + <div + class="blob-content" + > + <pre + class="code highlight" + > + <code + id="blob-code-content" + > + <span + id="LC1" + > + First + </span> + + + <span + id="LC2" + > + Second + </span> + + + <span + id="LC3" + > + Third + </span> + </code> + </pre> + </div> +</div> +`; diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js new file mode 100644 index 00000000000..17ea78b5826 --- /dev/null +++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js @@ -0,0 +1,27 @@ +import { shallowMount } from '@vue/test-utils'; +import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue'; + +describe('Blob Rich Viewer component', () => { + let wrapper; + const content = '<h1 id="markdown">Foo Bar</h1>'; + + function createComponent() { + wrapper = shallowMount(RichViewer, { + propsData: { + content, + }, + }); + } + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the passed content without transformations', () => { + expect(wrapper.html()).toContain(content); + }); +}); diff --git a/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js new file mode 100644 index 00000000000..d12bfc5c686 --- /dev/null +++ b/spec/frontend/vue_shared/components/blob_viewers/simple_viewer_spec.js @@ -0,0 +1,81 @@ +import { shallowMount } from '@vue/test-utils'; +import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue'; +import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants'; + +describe('Blob Simple Viewer component', () => { + let wrapper; + const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`; + + function createComponent(content = contentMock) { + wrapper = shallowMount(SimpleViewer, { + propsData: { + content, + }, + }); + } + + afterEach(() => { + wrapper.destroy(); + }); + + it('does not fail if content is empty', () => { + const spy = jest.spyOn(window.console, 'error'); + createComponent(''); + expect(spy).not.toHaveBeenCalled(); + }); + + describe('rendering', () => { + beforeEach(() => { + createComponent(); + }); + + it('matches the snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('renders exactly three lines', () => { + expect(wrapper.findAll('.js-line-number')).toHaveLength(3); + }); + + it('renders the content without transformations', () => { + expect(wrapper.html()).toContain(contentMock); + }); + }); + + describe('functionality', () => { + const scrollIntoViewMock = jest.fn(); + HTMLElement.prototype.scrollIntoView = scrollIntoViewMock; + + beforeEach(() => { + window.location.hash = '#LC2'; + createComponent(); + }); + + afterEach(() => { + window.location.hash = ''; + }); + + it('scrolls to requested line when rendered', () => { + const linetoBeHighlighted = wrapper.find('#LC2'); + expect(scrollIntoViewMock).toHaveBeenCalled(); + expect(wrapper.vm.highlightedLine).toBe(linetoBeHighlighted.element); + expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME); + }); + + it('switches highlighting when another line is selected', () => { + const currentlyHighlighted = wrapper.find('#LC2'); + const hash = '#LC3'; + const linetoBeHighlighted = wrapper.find(hash); + + expect(wrapper.vm.highlightedLine).toBe(currentlyHighlighted.element); + + wrapper.vm.scrollToLine(hash); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.vm.highlightedLine).toBe(linetoBeHighlighted.element); + expect(currentlyHighlighted.classes()).not.toContain(HIGHLIGHT_CLASS_NAME); + expect(linetoBeHighlighted.classes()).toContain(HIGHLIGHT_CLASS_NAME); + }); + }); + }); +}); diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb index ca0360b363e..b72fbc9fd3c 100644 --- a/spec/helpers/environments_helper_spec.rb +++ b/spec/helpers/environments_helper_spec.rb @@ -20,7 +20,7 @@ describe EnvironmentsHelper do expect(metrics_data).to include( 'settings-path' => edit_project_service_path(project, 'prometheus'), 'clusters-path' => project_clusters_path(project), - 'current-environment-name': environment.name, + 'current-environment-name' => environment.name, 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'), 'empty-getting-started-svg-path' => match_asset_path('/assets/illustrations/monitoring/getting_started.svg'), 'empty-loading-svg-path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'), |