Commit 26a50872 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent b3a736ed
<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>
<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>
......@@ -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>
......
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,
......
fragment BlobViewer on SnippetBlobViewer {
collapsed
loadingPartialName
renderError
tooLarge
type
fileType
}
......@@ -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>
query SnippetBlobContent($ids: [ID!], $rich: Boolean!) {
snippets(ids: $ids) {
edges {
node {
id
blob {
richData @include(if: $rich)
plainData @skip(if: $rich)
}
}
}
}
}
export const HIGHLIGHT_CLASS_NAME = 'hll';
export default {};
import RichViewer from './rich_viewer.vue';
import SimpleViewer from './simple_viewer.vue';
export { RichViewer, SimpleViewer };
export default {
props: {
content: {
type: String,
required: true,
},
},
};
<script>
import ViewerMixin from './mixins';
export default {
mixins: [ViewerMixin],
};
</script>
<template>
<div v-html="content"></div>
</template>
<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>
......@@ -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)
......
......@@ -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;
......
......@@ -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
......
# 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'),
......
......@@ -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 }
......
......@@ -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
......@@ -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
---
title: Move insights charts to echarts
merge_request: 24661
author:
type: other
---
title: Refactored snippets view to Vue
merge_request: 25188
author:
type: other
---
title: Update loading icon in Value Stream Analytics view
merge_request: 24861
author:
type: other
---
title: Fix code line and line number alignment in Safari
merge_request: 24820
author:
type: fixed
# Epics API **(ULTIMATE)**
# Epics API **(PREMIUM)**
Every API call to epic must be authenticated.
......
......@@ -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:
......
......@@ -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*) | ✓ | ✓ |
......
......@@ -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) |
......
......@@ -2,26 +2,21 @@
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|
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
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)
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')
......@@ -33,6 +28,32 @@ module QA
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
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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))
......
......@@ -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
......
......@@ -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
......
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);
});
});
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);
});
});
});
......@@ -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');
});
});
});
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 {};
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);
});
});
});
});
});
});
// 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>
`;
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);
});
});
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);
});
});
});
});
......@@ -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'),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment