提交 3902d464 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 d7ed3b47
......@@ -130,10 +130,13 @@ export default class MilestoneSelect {
fieldName: $dropdown.data('fieldName'),
text: milestone => escape(milestone.title),
id: milestone => {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
if (milestone !== undefined) {
if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
}
return milestone.id;
}
return milestone.id;
},
hidden: () => {
$selectBox.hide();
......
......@@ -49,7 +49,7 @@ export default {
<template>
<div class="d-flex flex-grow-1 flex-column h-100">
<edit-header class="py-2" :title="title" />
<rich-content-editor v-model="editableContent" class="mb-9" />
<rich-content-editor v-model="editableContent" class="mb-9 h-100" />
<publish-toolbar
class="gl-fixed gl-left-0 gl-bottom-0 gl-w-full"
:return-url="returnUrl"
......
......@@ -158,7 +158,7 @@ export default {
<div class="d-inline-block ml-md-2 ml-0">
<toolbar-button
:prepend="true"
tag="* "
tag="- "
:button-title="__('Add a bullet list')"
icon="list-bulleted"
/>
......@@ -170,7 +170,7 @@ export default {
/>
<toolbar-button
:prepend="true"
tag="* [ ] "
tag="- [ ] "
:button-title="__('Add a task list')"
icon="list-task"
/>
......
......@@ -24,6 +24,7 @@ const TOOLBAR_ITEM_CONFIGS = [
{ isDivider: true },
{ icon: 'dash', command: 'HR', tooltip: __('Add a line') },
{ icon: 'table', event: 'openPopupAddTable', classes: 'tui-table', tooltip: __('Add a table') },
{ icon: 'doc-image', event: CUSTOM_EVENTS.openAddImageModal, tooltip: __('Insert an image') },
{ isDivider: true },
{ icon: 'code', command: 'Code', tooltip: __('Insert inline code') },
];
......
......@@ -34,3 +34,10 @@ export const addCustomEventListener = (editorInstance, event, handler) => {
editorInstance.eventManager.addEventType(event);
editorInstance.eventManager.listen(event, handler);
};
export const removeCustomEventListener = (editorInstance, event, handler) =>
editorInstance.eventManager.removeEventHandler(event, handler);
export const addImage = ({ editor }, image) => editor.exec('AddImage', image);
export const getMarkdown = editorInstance => editorInstance.invoke('getMarkdown');
<script>
import { isSafeURL } from '~/lib/utils/url_utility';
import { GlModal, GlFormGroup, GlFormInput } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlModal,
GlFormGroup,
GlFormInput,
},
data() {
return {
error: null,
imageUrl: null,
altText: null,
modalTitle: __('Image Details'),
okTitle: __('Insert'),
urlLabel: __('Image URL'),
descriptionLabel: __('Description'),
};
},
methods: {
show() {
this.error = null;
this.imageUrl = null;
this.altText = null;
this.$refs.modal.show();
},
onOk(event) {
if (!this.isValid()) {
event.preventDefault();
return;
}
const { imageUrl, altText } = this;
this.$emit('addImage', { imageUrl, altText: altText || __('image') });
},
isValid() {
if (!isSafeURL(this.imageUrl)) {
this.error = __('Please provide a valid URL');
this.$refs.urlInput.$el.focus();
return false;
}
return true;
},
},
};
</script>
<template>
<gl-modal
ref="modal"
modal-id="add-image-modal"
:title="modalTitle"
:ok-title="okTitle"
@ok="onOk"
>
<gl-form-group
:label="urlLabel"
label-for="url-input"
:state="!Boolean(error)"
:invalid-feedback="error"
>
<gl-form-input id="url-input" ref="urlInput" v-model="imageUrl" />
</gl-form-group>
<gl-form-group :label="descriptionLabel" label-for="description-input">
<gl-form-input id="description-input" ref="descriptionInput" v-model="altText" />
</gl-form-group>
</gl-modal>
</template>
......@@ -2,6 +2,7 @@
import 'codemirror/lib/codemirror.css';
import '@toast-ui/editor/dist/toastui-editor.css';
import AddImageModal from './modals/add_image_modal.vue';
import {
EDITOR_OPTIONS,
EDITOR_TYPES,
......@@ -10,7 +11,12 @@ import {
CUSTOM_EVENTS,
} from './constants';
import { addCustomEventListener } from './editor_service';
import {
addCustomEventListener,
removeCustomEventListener,
addImage,
getMarkdown,
} from './editor_service';
export default {
components: {
......@@ -18,6 +24,7 @@ export default {
import(/* webpackChunkName: 'toast_editor' */ '@toast-ui/vue-editor').then(
toast => toast.Editor,
),
AddImageModal,
},
props: {
value: {
......@@ -49,13 +56,20 @@ export default {
editorOptions() {
return { ...EDITOR_OPTIONS, ...this.options };
},
editorInstance() {
return this.$refs.editor;
},
},
beforeDestroy() {
removeCustomEventListener(
this.editorInstance,
CUSTOM_EVENTS.openAddImageModal,
this.onOpenAddImageModal,
);
},
methods: {
onContentChanged() {
this.$emit('input', this.getMarkdown());
},
getMarkdown() {
return this.$refs.editor.invoke('getMarkdown');
this.$emit('input', getMarkdown(this.editorInstance));
},
onLoad(editorInstance) {
addCustomEventListener(
......@@ -65,20 +79,26 @@ export default {
);
},
onOpenAddImageModal() {
// TODO - add image modal (next MR)
this.$refs.addImageModal.show();
},
onAddImage(image) {
addImage(this.editorInstance, image);
},
},
};
</script>
<template>
<toast-editor
ref="editor"
:initial-value="value"
:options="editorOptions"
:preview-style="previewStyle"
:initial-edit-type="initialEditType"
:height="height"
@change="onContentChanged"
@load="onLoad"
/>
<div>
<toast-editor
ref="editor"
:initial-value="value"
:options="editorOptions"
:preview-style="previewStyle"
:initial-edit-type="initialEditType"
:height="height"
@change="onContentChanged"
@load="onLoad"
/>
<add-image-modal ref="addImageModal" @addImage="onAddImage" />
</div>
</template>
......@@ -15,7 +15,7 @@ module ImportState
def refresh_jid_expiration
return unless jid
Gitlab::SidekiqStatus.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
Gitlab::SidekiqStatus.set(jid, Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
end
def self.jid_by(project_id:, status:)
......
......@@ -4,9 +4,9 @@
= markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: _("Insert a quote") })
= markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: _("Insert code") })
= markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: _("Add a link") })
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: _("Add a bullet list") })
= markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "- ", "md-prepend" => true }, title: _("Add a bullet list") })
= markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: _("Add a numbered list") })
= markdown_toolbar_button({ icon: "list-task", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: _("Add a task list") })
= markdown_toolbar_button({ icon: "list-task", data: { "md-tag" => "- [ ] ", "md-prepend" => true }, title: _("Add a task list") })
= markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _("Add a table") })
- if show_fullscreen_button
%button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: _("Go full screen"), data: { container: "body" } }
......
......@@ -6,7 +6,7 @@ module ProjectImportOptions
IMPORT_RETRY_COUNT = 5
included do
sidekiq_options retry: IMPORT_RETRY_COUNT, status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
sidekiq_options retry: IMPORT_RETRY_COUNT, status_expiration: Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION
# We only want to mark the project as failed once we exhausted all retries
sidekiq_retries_exhausted do |job|
......
......@@ -3,8 +3,6 @@
class StuckImportJobsWorker # rubocop:disable Scalability/IdempotentWorker
include Gitlab::Import::StuckImportJob
IMPORT_JOBS_EXPIRATION = Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION
private
def track_metrics(with_jid_count, without_jid_count)
......
---
title: Add ability to insert an image via SSE
merge_request: 33029
author:
type: added
---
title: Add index on id and type for Snippets
merge_request: 32885
author:
type: performance
---
title: Add support for artifacts/exclude configuration
merge_request: 33170
author:
type: added
---
title: Set markdown toolbar to use hyphens for lists
merge_request: 31426
author:
type: changed
......@@ -65,7 +65,23 @@ module.exports = {
}),
new YarnCheck({
rootDirectory: ROOT_PATH,
exclude: /ts-jest/,
exclude: new RegExp(
[
/*
chokidar has a newer version which do not depend on fsevents,
is faster and only compatible with newer node versions (>=8)
Their actual interface remains the same and we can safely _force_
newer versions to get performance and security benefits.
This can be removed once all dependencies are up to date:
https://gitlab.com/gitlab-org/gitlab/-/issues/219353
*/
'chokidar',
// We are ignoring ts-jest, because we force a newer version, compatible with our current jest version
'ts-jest',
].join('|'),
),
forceKill: true,
}),
],
......
# frozen_string_literal: true
class AddIndexOnSnippetTypeAndId < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :snippets, [:id, :type]
end
def down
remove_concurrent_index :snippets, [:id, :type]
end
end
......@@ -10745,6 +10745,8 @@ CREATE INDEX index_snippets_on_description_trigram ON public.snippets USING gin
CREATE INDEX index_snippets_on_file_name_trigram ON public.snippets USING gin (file_name public.gin_trgm_ops);
CREATE INDEX index_snippets_on_id_and_type ON public.snippets USING btree (id, type);
CREATE INDEX index_snippets_on_project_id_and_visibility_level ON public.snippets USING btree (project_id, visibility_level);
CREATE INDEX index_snippets_on_title_trigram ON public.snippets USING gin (title public.gin_trgm_ops);
......@@ -14025,6 +14027,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200521225327
20200521225337
20200521225346
20200522235146
20200525114553
20200525121014
20200526000407
......
......@@ -117,7 +117,7 @@ The following table lists available parameters for jobs:
| [`when`](#when) | When to run job. Also available: `when:manual` and `when:delayed`. |
| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in` and `environment:action`. |
| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. |
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:junit`, `artifacts:reports:cobertura`, and `artifacts:reports:terraform`.<br><br>In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_scanning`, `artifacts:reports:license_management` (removed in GitLab 13.0),`artifacts:reports:performance` and `artifacts:reports:metrics`. |
| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:junit`, `artifacts:reports:cobertura`, and `artifacts:reports:terraform`.<br><br>In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_scanning`, `artifacts:reports:license_management` (removed in GitLab 13.0),`artifacts:reports:performance` and `artifacts:reports:metrics`. |
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
| [`coverage`](#coverage) | Code coverage settings for a given job. |
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
......@@ -2555,6 +2555,33 @@ job:
- path/*xyz/*
```
#### `artifacts:exclude`
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/15122) in GitLab 13.1
> - Requires GitLab Runner 13.1
`exclude` makes it possible to prevent files from being added to an artifacts
archive.
Similar to [`artifacts:paths`](#artifactspaths), `exclude` paths are relative
to the project directory. Wildcards can be used that follow the
[glob](https://en.wikipedia.org/wiki/Glob_(programming)) patterns and
[`filepath.Match`](https://golang.org/pkg/path/filepath/#Match).
For example, to store all files in `binaries/`, but not `*.o` files located in
subdirectories of `binaries/`:
```yaml
artifacts:
paths:
- binaries/
exclude:
- binaries/**/*.o
```
Files matched by [`artifacts:untracked`](#artifactsuntracked) can be excluded using
`artifacts:exclude` too.
#### `artifacts:expose_as`
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15018) in GitLab 12.5.
......@@ -2701,6 +2728,15 @@ artifacts:
- binaries/
```
Send all untracked files but [exclude](#artifactsexclude) `*.txt`:
```yaml
artifacts:
untracked: true
exclude:
- *.txt
```
#### `artifacts:when`
> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
......
......@@ -7,7 +7,7 @@ module Gitlab
#
module Features
def self.artifacts_exclude_enabled?
::Feature.enabled?(:ci_artifacts_exclude, default_enabled: false)
::Feature.enabled?(:ci_artifacts_exclude, default_enabled: true)
end
def self.ensure_scheduling_type_enabled?
......
......@@ -14,7 +14,7 @@ module Gitlab
jid = generate_jid(import_state)
Gitlab::SidekiqStatus
.set(jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
.set(jid, Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
import_state.update_column(:jid, jid)
end
......
......@@ -63,7 +63,7 @@ module Gitlab
def timeout
# Setting the timeout to the same one as we do for clearing stuck jobs
# this makes sure all cache is available while the import is running.
StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION
end
end
end
......
......@@ -40,7 +40,7 @@ module Gitlab
def timeout
# Make sure we get rid of all the information after a job is marked
# as failed/succeeded
StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION
end
end
end
......
......@@ -133,6 +133,8 @@ module Gitlab
releases: count(Release),
remote_mirrors: count(RemoteMirror),
snippets: count(Snippet),
personal_snippets: count(PersonalSnippet),
project_snippets: count(ProjectSnippet),
suggestions: count(Suggestion),
terraform_reports: count(::Ci::JobArtifact.terraform_reports),
terraform_states: count(::Terraform::State),
......
......@@ -11600,6 +11600,12 @@ msgstr ""
msgid "Ignored"
msgstr ""
msgid "Image Details"
msgstr ""
msgid "Image URL"
msgstr ""
msgid "Image: %{image}"
msgstr ""
......@@ -11858,12 +11864,18 @@ msgstr ""
msgid "Input your repository URL"
msgstr ""
msgid "Insert"
msgstr ""
msgid "Insert a code block"
msgstr ""
msgid "Insert a quote"
msgstr ""
msgid "Insert an image"
msgstr ""
msgid "Insert code"
msgstr ""
......@@ -16038,6 +16050,9 @@ msgstr ""
msgid "Please provide a name"
msgstr ""
msgid "Please provide a valid URL"
msgstr ""
msgid "Please provide a valid email address."
msgstr ""
......@@ -26260,6 +26275,9 @@ msgstr ""
msgid "https://your-bitbucket-server"
msgstr ""
msgid "image"
msgstr ""
msgid "image diff"
msgstr ""
......
......@@ -194,7 +194,7 @@
"markdownlint-cli": "0.18.0",
"md5": "^2.2.1",
"node-sass": "^4.12.0",
"nodemon": "^1.18.9",
"nodemon": "^2.0.4",
"pixelmatch": "^4.0.2",
"postcss": "^7.0.14",
"prettier": "1.18.2",
......@@ -212,8 +212,9 @@
"bootstrap-vue": "https://docs.gitlab.com/ee/development/fe_guide/dependencies.md#bootstrapvue"
},
"resolutions": {
"vue-jest/ts-jest": "24.0.0",
"monaco-editor": "0.18.1"
"chokidar": "^3.4.0",
"monaco-editor": "0.18.1",
"vue-jest/ts-jest": "24.0.0"
},
"engines": {
"node": ">=10.13.0",
......
FROM ruby:2.6-stretch
LABEL maintainer="GitLab Quality Department <quality@gitlab.com>"
ENV DEBIAN_FRONTEND noninteractive
ENV DEBIAN_FRONTEND="noninteractive"
ENV DOCKER_VERSION="17.09.0-ce"
ENV CHROME_VERSION="83.0.4103.61-1"
ENV CHROME_DRIVER_VERSION="83.0.4103.39"
ENV CHROME_DEB="google-chrome-stable_${CHROME_VERSION}_amd64.deb"
ENV CHROME_URL="https://s3.amazonaws.com/gitlab-google-chrome-stable/${CHROME_DEB}"
ENV K3D_VERSION="1.3.4"
##
# Add support for stretch-backports
......@@ -21,28 +28,31 @@ RUN apt-get -y -t stretch-backports install git git-lfs
##
# Install Docker
#
RUN wget -q https://download.docker.com/linux/static/stable/x86_64/docker-17.09.0-ce.tgz && \
tar -zxf docker-17.09.0-ce.tgz && mv docker/docker /usr/local/bin/docker && \
rm docker-17.09.0-ce.tgz
RUN wget -q "https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz" && \
tar -zxf "docker-${DOCKER_VERSION}.tgz" && mv docker/docker /usr/local/bin/docker && \
rm "docker-${DOCKER_VERSION}.tgz"
##
# Install Google Chrome version with headless support
# Download from our local S3 bucket, populated by https://gitlab.com/gitlab-org/gitlab-build-images/-/blob/master/scripts/cache-google-chrome
#
RUN curl -sS -L https://dl.google.com/linux/linux_signing_key.pub | apt-key add -
RUN echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
RUN apt-get update -q && apt-get install -y google-chrome-stable && apt-get clean
RUN curl --silent --show-error --fail -O "${CHROME_URL}" && \
dpkg -i "./${CHROME_DEB}" || true && \
apt-get install -f -y && \
rm -f "./${CHROME_DEB}"
##
# Install chromedriver to make it work with Selenium
#
RUN wget -q https://chromedriver.storage.googleapis.com/$(wget -q -O - https://chromedriver.storage.googleapis.com/LATEST_RELEASE)/chromedriver_linux64.zip
RUN wget -q "https://chromedriver.storage.googleapis.com/${CHROME_DRIVER_VERSION}/chromedriver_linux64.zip"
RUN unzip chromedriver_linux64.zip -d /usr/local/bin
RUN rm -f chromedriver_linux64.zip
##
# Install K3d local cluster support
# https://github.com/rancher/k3d
#
RUN curl -s https://raw.githubusercontent.com/rancher/k3d/master/install.sh | TAG=v1.3.4 bash
RUN curl -s https://raw.githubusercontent.com/rancher/k3d/master/install.sh | TAG="v${K3D_VERSION}" bash
##
# Install gcloud and kubectl CLI used in Auto DevOps test to create K8s
......
......@@ -43,6 +43,7 @@ module QA
module API
autoload :Client, 'qa/runtime/api/client'
autoload :RepositoryStorageMoves, 'qa/runtime/api/repository_storage_moves'
autoload :Request, 'qa/runtime/api/request'
end
......
......@@ -91,6 +91,10 @@ module QA
super
end
def has_file?(file_path)
repository_tree.any? { |file| file[:path] == file_path }
end
def api_get_path
"/projects/#{CGI.escape(path_with_namespace)}"
end
......@@ -115,6 +119,10 @@ module QA
"#{api_get_path}/repository/branches"
end
def api_repository_tree_path
"#{api_get_path}/repository/tree"
end
def api_pipelines_path
"#{api_get_path}/pipelines"
end
......@@ -155,11 +163,9 @@ module QA
raise ResourceUpdateFailedError, "Could not change repository storage to #{new_storage}. Request returned (#{response.code}): `#{response}`."
end
wait_until do
reload!
api_response[:repository_storage] == new_storage
end
wait_until(sleep_interval: 1) { Runtime::API::RepositoryStorageMoves.has_status?(self, 'finished') }
rescue Support::Repeater::RepeaterConditionExceededError
raise Runtime::API::RepositoryStorageMoves::RepositoryStorageMovesError, 'Timed out while waiting for the repository storage move to finish'
end
def import_status
......@@ -187,8 +193,11 @@ module QA
end
def repository_branches
response = get Runtime::API::Request.new(api_client, api_repository_branches_path).url
parse_body(response)
parse_body(get(Runtime::API::Request.new(api_client, api_repository_branches_path).url))
end
def repository_tree
parse_body(get(Runtime::API::Request.new(api_client, api_repository_tree_path).url))
end
def pipelines
......
# frozen_string_literal: true
module QA
module Runtime
module API
module RepositoryStorageMoves
extend self
extend Support::Api
RepositoryStorageMovesError = Class.new(RuntimeError)
def has_status?(project, status)
all.any? do |move|
move[:project][:path_with_namespace] == project.path_with_namespace &&
move[:state] == status &&
move[:destination_storage_name] == Env.additional_repository_storage
end
end
def all
Logger.debug('Getting repository storage moves')
parse_body(get(Request.new(api_client, '/project_repository_storage_moves').url))
end
private
def api_client
@api_client ||= Client.as_admin
end
end
end
end
end
# frozen_string_literal: true
module QA
context 'Create' do
describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'repo-storage-status'
project.initialize_with_readme = true
end
end
it 'confirms a `finished` status after moving project repository storage' do
expect(project).to have_file('README.md')
project.change_repository_storage(QA::Runtime::Env.additional_repository_storage)
expect(Runtime::API::RepositoryStorageMoves).to have_status(project, 'finished')
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = 'new_file'
push.file_content = '# This is a new file'
push.commit_message = 'Add new file'
push.new_branch = false
end
expect(project).to have_file('README.md')
expect(project).to have_file('new_file')
end
end
end
end
......@@ -7,8 +7,9 @@ module QA
module Repeater
DEFAULT_MAX_WAIT_TIME = 60
RetriesExceededError = Class.new(RuntimeError)
WaitExceededError = Class.new(RuntimeError)
RepeaterConditionExceededError = Class.new(RuntimeError)
RetriesExceededError = Class.new(RepeaterConditionExceededError)
WaitExceededError = Class.new(RepeaterConditionExceededError)
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
......
......@@ -25,13 +25,13 @@ describe('init markdown', () => {
insertMarkdownText({
textArea,
text: textArea.value,
tag: '* ',
tag: '- ',
blockTag: null,
selected: '',
wrap: false,
});
expect(textArea.value).toEqual(`${initialValue}* `);
expect(textArea.value).toEqual(`${initialValue}- `);
});
it('inserts the tag on a new line if the current one is not empty', () => {
......@@ -43,13 +43,13 @@ describe('init markdown', () => {
insertMarkdownText({
textArea,
text: textArea.value,
tag: '* ',
tag: '- ',
blockTag: null,
selected: '',
wrap: false,
});
expect(textArea.value).toEqual(`${initialValue}\n* `);
expect(textArea.value).toEqual(`${initialValue}\n- `);
});
it('inserts the tag on the same line if the current line only contains spaces', () => {
......@@ -61,13 +61,13 @@ describe('init markdown', () => {
insertMarkdownText({
textArea,
text: textArea.value,
tag: '* ',
tag: '- ',
blockTag: null,
selected: '',
wrap: false,
});
expect(textArea.value).toEqual(`${initialValue}* `);
expect(textArea.value).toEqual(`${initialValue}- `);
});
it('inserts the tag on the same line if the current line only contains tabs', () => {
......@@ -79,13 +79,13 @@ describe('init markdown', () => {
insertMarkdownText({
textArea,
text: textArea.value,
tag: '* ',
tag: '- ',
blockTag: null,
selected: '',
wrap: false,
});
expect(textArea.value).toEqual(`${initialValue}* `);
expect(textArea.value).toEqual(`${initialValue}- `);
});
it('places the cursor inside the tags', () => {
......
......@@ -185,7 +185,7 @@ describe('Markdown field component', () => {
markdownButton.trigger('click');
return wrapper.vm.$nextTick(() => {
expect(textarea.value).toContain('* testing');
expect(textarea.value).toContain('- testing');
});
});
......@@ -197,7 +197,7 @@ describe('Markdown field component', () => {
markdownButton.trigger('click');
return wrapper.vm.$nextTick(() => {
expect(textarea.value).toContain('* testing\n* 123');
expect(textarea.value).toContain('- testing\n- 123');
});
});
});
......
import {
generateToolbarItem,
addCustomEventListener,
removeCustomEventListener,
addImage,
getMarkdown,
} from '~/vue_shared/components/rich_content_editor/editor_service';
describe('Editor Service', () => {
const mockInstance = {
eventManager: { addEventType: jest.fn(), removeEventHandler: jest.fn(), listen: jest.fn() },
editor: { exec: jest.fn() },
invoke: jest.fn(),
};
const event = 'someCustomEvent';
const handler = jest.fn();
describe('generateToolbarItem', () => {
const config = {
icon: 'bold',
......@@ -11,6 +22,7 @@ describe('Editor Service', () => {
tooltip: 'Some Tooltip',
event: 'some-event',
};
const generatedItem = generateToolbarItem(config);
it('generates the correct command', () => {
......@@ -33,10 +45,6 @@ describe('Editor Service', () => {
});
describe('addCustomEventListener', () => {
const mockInstance = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } };
const event = 'someCustomEvent';
const handler = jest.fn();
it('registers an event type on the instance and adds an event handler', () => {
addCustomEventListener(mockInstance, event, handler);
......@@ -44,4 +52,30 @@ describe('Editor Service', () => {
expect(mockInstance.eventManager.listen).toHaveBeenCalledWith(event, handler);
});
});
describe('removeCustomEventListener', () => {
it('removes an event handler from the instance', () => {
removeCustomEventListener(mockInstance, event, handler);
expect(mockInstance.eventManager.removeEventHandler).toHaveBeenCalledWith(event, handler);
});
});
describe('addImage', () => {
it('calls the exec method on the instance', () => {
const mockImage = { imageUrl: 'some/url.png', description: 'some description' };
addImage(mockInstance, mockImage);
expect(mockInstance.editor.exec).toHaveBeenCalledWith('AddImage', mockImage);
});
});
describe('getMarkdown', () => {
it('calls the invoke method on the instance', () => {
getMarkdown(mockInstance);
expect(mockInstance.invoke).toHaveBeenCalledWith('getMarkdown');
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image_modal.vue';
describe('Add Image Modal', () => {
let wrapper;
const findModal = () => wrapper.find(GlModal);
const findUrlInput = () => wrapper.find({ ref: 'urlInput' });
const findDescriptionInput = () => wrapper.find({ ref: 'descriptionInput' });
beforeEach(() => {
wrapper = shallowMount(AddImageModal);
});
describe('when content is loaded', () => {
it('renders a modal component', () => {
expect(findModal().exists()).toBe(true);
});
it('renders an input to add an image URL', () => {
expect(findUrlInput().exists()).toBe(true);
});
it('renders an input to add an image description', () => {
expect(findDescriptionInput().exists()).toBe(true);
});
});
describe('add image', () => {
it('emits an addImage event when a valid URL is specified', () => {
const preventDefault = jest.fn();
const mockImage = { imageUrl: '/some/valid/url.png', altText: 'some description' };
wrapper.setData({ ...mockImage });
findModal().vm.$emit('ok', { preventDefault });
expect(preventDefault).not.toHaveBeenCalled();
expect(wrapper.emitted('addImage')).toEqual([[mockImage]]);
});
});
});
import { shallowMount } from '@vue/test-utils';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image_modal.vue';
import {
EDITOR_OPTIONS,
EDITOR_TYPES,
......@@ -8,11 +9,17 @@ import {
CUSTOM_EVENTS,
} from '~/vue_shared/components/rich_content_editor/constants';
import { addCustomEventListener } from '~/vue_shared/components/rich_content_editor/editor_service';
import {
addCustomEventListener,
removeCustomEventListener,
addImage,
} from '~/vue_shared/components/rich_content_editor/editor_service';
jest.mock('~/vue_shared/components/rich_content_editor/editor_service', () => ({
...jest.requireActual('~/vue_shared/components/rich_content_editor/editor_service'),
addCustomEventListener: jest.fn(),
removeCustomEventListener: jest.fn(),
addImage: jest.fn(),
}));
describe('Rich Content Editor', () => {
......@@ -20,6 +27,7 @@ describe('Rich Content Editor', () => {
const value = '## Some Markdown';
const findEditor = () => wrapper.find({ ref: 'editor' });
const findAddImageModal = () => wrapper.find(AddImageModal);
beforeEach(() => {
wrapper = shallowMount(RichContentEditor, {
......@@ -77,4 +85,34 @@ describe('Rich Content Editor', () => {
);
});
});
describe('when editor is destroyed', () => {
it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
const mockInstance = { eventManager: { removeEventHandler: jest.fn() } };
wrapper.vm.$refs.editor = mockInstance;
wrapper.vm.$destroy();
expect(removeCustomEventListener).toHaveBeenCalledWith(
mockInstance,
CUSTOM_EVENTS.openAddImageModal,
wrapper.vm.onOpenAddImageModal,
);
});
});
describe('add image modal', () => {
it('renders an addImageModal component', () => {
expect(findAddImageModal().exists()).toBe(true);
});
it('calls the onAddImage method when the addImage event is emitted', () => {
const mockImage = { imageUrl: 'some/url.png', description: 'some description' };
const mockInstance = { exec: jest.fn() };
wrapper.vm.$refs.editor = mockInstance;
findAddImageModal().vm.$emit('addImage', mockImage);
expect(addImage).toHaveBeenCalledWith(mockInstance, mockImage);
});
});
});
......@@ -8,7 +8,7 @@ describe Gitlab::Import::SetAsyncJid do
it 'sets the JID in Redis' do
expect(Gitlab::SidekiqStatus)
.to receive(:set)
.with("async-import/project-import-state/#{project.id}", StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
.with("async-import/project-import-state/#{project.id}", Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
.and_call_original
described_class.set_jid(project.import_state)
......
......@@ -66,7 +66,7 @@ describe Gitlab::PhabricatorImport::Cache::Map, :clean_gitlab_redis_cache do
end
expect(set_data).to eq({ classname: 'Issue', database_id: issue.id.to_s })
expect(ttl).to be_within(1.second).of(StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
expect(ttl).to be_within(1.second).of(Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
end
end
......
......@@ -124,6 +124,8 @@ module UsageDataHelpers
releases
remote_mirrors
snippets
personal_snippets
project_snippets
suggestions
terraform_reports
terraform_states
......
......@@ -17,7 +17,7 @@ describe ProjectImportOptions do
end
it 'sets default status expiration' do
expect(worker_class.sidekiq_options['status_expiration']).to eq(StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
expect(worker_class.sidekiq_options['status_expiration']).to eq(Gitlab::Import::StuckImportJob::IMPORT_JOBS_EXPIRATION)
end
describe '.sidekiq_retries_exhausted' do
......
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册