提交 51c20446 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 0c4b9cac
......@@ -83,7 +83,6 @@
- "{,ee/}fixtures/**/*"
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
.code-patterns: &code-patterns
- "{package.json,yarn.lock}"
......@@ -126,7 +125,6 @@
- "{,ee/}fixtures/**/*"
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
.code-qa-patterns: &code-qa-patterns
- "{package.json,yarn.lock}"
......@@ -168,7 +166,6 @@
- "{,ee/}fixtures/**/*"
- "{,ee/}rubocop/**/*"
- "{,ee/}spec/**/*"
- "doc/README.md" # Some RSpec test rely on this file
# QA changes
- ".dockerignore"
- "qa/**/*"
......
......@@ -495,3 +495,6 @@ gem 'mail', '= 2.7.1'
# File encryption
gem 'lockbox', '~> 0.3.3'
# Email validation
gem 'valid_email', '~> 0.1'
......@@ -842,7 +842,7 @@ GEM
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
rainbow (3.0.0)
raindrops (0.19.0)
raindrops (0.19.1)
rake (12.3.3)
rb-fsevent (0.10.2)
rb-inotify (0.9.10)
......@@ -1109,6 +1109,9 @@ GEM
equalizer (~> 0.0.9)
parser (>= 2.6.5)
procto (~> 0.0.2)
valid_email (0.1.3)
activemodel
mail (>= 2.6.1)
validate_email (0.1.6)
activemodel (>= 3.0)
mail (>= 2.2.5)
......@@ -1402,6 +1405,7 @@ DEPENDENCIES
unicorn (~> 5.5)
unicorn-worker-killer (~> 0.4.4)
unleash (~> 0.1.5)
valid_email (~> 0.1)
validates_hostname (~> 1.0.6)
version_sorter (~> 2.2.4)
vmstat (~> 2.3.0)
......
/* global ace */
import Editor from '~/editor/editor_lite';
export function initEditorLite({ el, blobPath, blobContent }) {
if (!el) {
throw new Error(`"el" parameter is required to initialize Editor`);
}
let editor;
if (window?.gon?.features?.monacoSnippets) {
editor = new Editor();
editor.createInstance({
el,
blobPath,
blobContent,
});
} else {
editor = ace.edit(el);
}
const editor = new Editor();
editor.createInstance({
el,
blobPath,
blobContent,
});
return editor;
}
......
......@@ -137,7 +137,7 @@ export default {
},
isDropdownOpen() {
return this.$el.classList.contains('open');
return this.$el.classList.contains('show');
},
pipelineActionRequestComplete() {
......
......@@ -3,7 +3,7 @@ import {
GlProgressBar,
GlLink,
GlBadge,
GlDeprecatedButton,
GlButton,
GlTooltipDirective,
GlSprintf,
} from '@gitlab/ui';
......@@ -17,7 +17,7 @@ export default {
GlProgressBar,
GlLink,
GlBadge,
GlDeprecatedButton,
GlButton,
GlSprintf,
},
directives: {
......@@ -134,13 +134,9 @@ export default {
<span :key="'bullet-' + milestone.id" class="append-right-4">&bull;</span>
</template>
<template v-if="shouldRenderShowMoreLink(index)">
<gl-deprecated-button
:key="'more-button-' + milestone.id"
variant="link"
@click="toggleShowAll"
>
<gl-button :key="'more-button-' + milestone.id" variant="link" @click="toggleShowAll">
{{ moreText }}
</gl-deprecated-button>
</gl-button>
</template>
</template>
</div>
......
......@@ -3,18 +3,6 @@ import setupCollapsibleInputs from './collapsible_input';
let editor;
const initAce = () => {
const editorEl = document.getElementById('editor');
const form = document.querySelector('.snippet-form-holder form');
const content = document.querySelector('.snippet-file-content');
editor = initEditorLite({ el: editorEl });
form.addEventListener('submit', () => {
content.value = editor.getValue();
});
};
const initMonaco = () => {
const editorEl = document.getElementById('editor');
const contentEl = document.querySelector('.snippet-file-content');
......@@ -36,15 +24,7 @@ const initMonaco = () => {
});
};
export const initEditor = () => {
if (window?.gon?.features?.monacoSnippets) {
initMonaco();
} else {
initAce();
}
setupCollapsibleInputs();
};
export default () => {
initEditor();
initMonaco();
setupCollapsibleInputs();
};
......@@ -6,7 +6,8 @@ class Email < ApplicationRecord
belongs_to :user, optional: false
validates :email, presence: true, uniqueness: true, devise_email: true
validates :email, presence: true, uniqueness: true
validate :validate_email_format
validate :unique_email, if: ->(email) { email.email_changed? }
scope :confirmed, -> { where.not(confirmed_at: nil) }
......@@ -30,6 +31,10 @@ class Email < ApplicationRecord
user.accept_pending_invitations!
end
def validate_email_format
self.errors.add(:email, I18n.t(:invalid, scope: 'valid_email.validations.email')) unless ValidateEmail.valid?(self.email)
end
# once email is confirmed, update the gpg signatures
def update_invalid_gpg_signatures
user.update_invalid_gpg_signatures if confirmed?
......
- if Feature.disabled?(:monaco_snippets)
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- if Feature.enabled?(:snippets_edit_vue)
#js-snippet-edit.snippet-form{ data: {'project_path': @snippet.project&.full_path, 'snippet-gid': @snippet.new_record? ? '' : @snippet.to_global_id, 'markdown-preview-path': preview_markdown_path(parent), 'markdown-docs-path': help_page_path('user/markdown'), 'visibility-help-link': help_page_path("public_access/public_access") } }
- else
......
---
title: Updated deprecated buttons in release page
merge_request: 30941
author: Özgür Adem Işıklı @iozguradem
type: added
---
title: Change validation rules for profile email addresses
merge_request: 30633
author:
type: fixed
---
title: Enable Monaco for editing Snippets by default
merge_request: 30892
author:
type: added
# Project Vulnerabilities API **(ULTIMATE)**
# Vulnerability export API **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/197494) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/197494) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10. [Updated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30397) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
CAUTION: **Caution:**
This API is currently in development and is protected by a **disabled**
......@@ -17,21 +17,21 @@ across GitLab releases.
Every API call to vulnerability exports must be [authenticated](README.md#authentication).
## Create a project-level vulnerability export
Creates a new vulnerability export for a project.
Vulnerability export permissions inherit permissions from their project. If a project is
private and a user isn't a member of the project to which the vulnerability
belongs, requests to that project return a `404 Not Found` status code.
Vulnerability exports can be only accessed by the export's author.
## Create vulnerability export
Creates a new vulnerability export.
If an authenticated user doesn't have permission to
[create a new vulnerability](../user/permissions.md#project-members-permissions),
this request results in a `403` status code.
```plaintext
POST /projects/:id/vulnerability_exports
POST /security/projects/:id/vulnerability_exports
```
| Attribute | Type | Required | Description |
......@@ -39,7 +39,7 @@ POST /projects/:id/vulnerability_exports
| `id` | integer or string | yes | The ID or [URL-encoded path](README.md#namespaced-path-encoding) of the project which the authenticated user is a member of |
```shell
curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports
curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/projects/1/vulnerability_exports
```
The created vulnerability export will be automatically deleted after 1 hour.
......@@ -56,8 +56,40 @@ Example response:
"started_at": null,
"finished_at": null,
"_links": {
"self": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2",
"download": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download"
"self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2",
"download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download"
}
}
```
## Create an instance-level vulnerability export
Creates a new vulnerability export for the projects of the user selected in the Security Dashboard.
```plaintext
POST /security/vulnerability_exports
```
```shell
curl --header POST "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports
```
The created vulnerability export is automatically deleted after one hour.
Example response:
```json
{
"id": 2,
"created_at": "2020-03-30T09:35:38.746Z",
"project_id": null,
"format": "csv",
"status": "created",
"started_at": null,
"finished_at": null,
"_links": {
"self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2",
"download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download"
}
}
```
......@@ -67,16 +99,15 @@ Example response:
Gets a single vulnerability export.
```plaintext
POST /projects/:id/vulnerability_exports/:vulnerability_export_id
GET /security/vulnerability_exports/:id
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer or string | yes | The vulnerability's ID |
| `vulnerability_export_id` | integer or string | yes | The vulnerability export's ID |
| `id` | integer or string | yes | The vulnerability export's ID |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports/2
```
If the vulnerability export isn't finished, the response is `202 Accepted`.
......@@ -93,8 +124,8 @@ Example response:
"started_at": "2020-03-30T09:36:54.469Z",
"finished_at": "2020-03-30T09:36:55.008Z",
"_links": {
"self": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2",
"download": "https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download"
"self": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2",
"download": "https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download"
}
}
```
......@@ -104,16 +135,15 @@ Example response:
Downloads a single vulnerability export.
```plaintext
POST /projects/:id/vulnerability_exports/:vulnerability_export_id/download
GET /security/vulnerability_exports/:id/download
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer or string | yes | The vulnerability's ID |
| `vulnerability_export_id` | integer or string | yes | The vulnerability export's ID |
| `id` | integer or string | yes | The vulnerability export's ID |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/vulnerability_exports/2/download
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/security/vulnerability_exports/2/download
```
The response will be `404 Not Found` if the vulnerability export is not finished yet or was not found.
......
......@@ -405,8 +405,6 @@ merge request with new or changed docs is submitted, are:
- [`internal_anchors`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L69)
checks that all internal anchors (ex: `[link](../index.md#internal_anchor)`)
are valid.
- If any code or the `doc/README.md` file is changed, a full pipeline will run, which
runs tests for [`/help`](#gitlab-help-tests).
### Running tests
......
......@@ -20,8 +20,8 @@ The Packages feature allows GitLab to act as a repository for the following:
If you cannot find the **{package}** **Packages > List** entry under your
project's sidebar, it is not enabled in your GitLab instance. Ask your
administrator to enable GitLab Package Registry following the administration
documentation.
administrator to enable GitLab Package Registry following the [administration
documentation](../../administration/packages/index.md).
Once enabled for your GitLab instance, to enable Package Registry for your
project:
......
......@@ -150,6 +150,7 @@ The following table depicts the various user permission levels in a project.
| Manage [push rules](../push_rules/push_rules.md) | | | | ✓ | ✓ |
| Switch visibility level | | | | | ✓ |
| Transfer project to another namespace | | | | | ✓ |
| Remove fork relationship | | | | | ✓ |
| Remove project | | | | | ✓ |
| Delete issues | | | | | ✓ |
| Disable notification emails | | | | | ✓ |
......
......@@ -43,7 +43,6 @@ module Gitlab
# Initialize gon.features with any flags that should be
# made globally available to the frontend
push_frontend_feature_flag(:snippets_vue, default_enabled: false)
push_frontend_feature_flag(:monaco_snippets, default_enabled: false)
push_frontend_feature_flag(:monaco_blobs, default_enabled: false)
push_frontend_feature_flag(:monaco_ci, default_enabled: false)
push_frontend_feature_flag(:snippets_edit_vue, default_enabled: false)
......
......@@ -10,12 +10,20 @@ describe Profiles::EmailsController do
end
describe '#create' do
let(:email_params) { { email: "add_email@example.com" } }
context 'when email address is valid' do
let(:email_params) { { email: "add_email@example.com" } }
it 'sends an email confirmation' do
expect { post(:create, params: { email: email_params }) }.to change { ActionMailer::Base.deliveries.size }
expect(ActionMailer::Base.deliveries.last.to).to eq [email_params[:email]]
expect(ActionMailer::Base.deliveries.last.subject).to match "Confirmation instructions"
it 'sends an email confirmation' do
expect { post(:create, params: { email: email_params }) }.to change { ActionMailer::Base.deliveries.size }
end
end
context 'when email address is invalid' do
let(:email_params) { { email: "test.@example.com" } }
it 'does not send an email confirmation' do
expect { post(:create, params: { email: email_params }) }.not_to change { ActionMailer::Base.deliveries.size }
end
end
end
......
......@@ -4,35 +4,9 @@ require 'spec_helper'
describe 'Help Pages' do
describe 'Get the main help page' do
shared_examples_for 'help page' do |prefix: ''|
it 'prefixes links correctly' do
expect(page).to have_selector(%(div.documentation-index > table tbody tr td a[href="#{prefix}/help/api/README.md"]))
end
end
context 'without a trailing slash' do
before do
visit help_path
end
it_behaves_like 'help page'
end
context 'with a trailing slash' do
before do
visit help_path + '/'
end
it_behaves_like 'help page'
end
context 'with a relative installation' do
before do
stub_config_setting(relative_url_root: '/gitlab')
visit help_path
end
it_behaves_like 'help page', prefix: '/gitlab'
before do
allow(File).to receive(:read).and_call_original
allow(File).to receive(:read).with(Rails.root.join('doc', 'README.md')).and_return(fixture_file('sample_doc.md'))
end
context 'quick link shortcuts', :js do
......
......@@ -31,6 +31,15 @@ describe 'Profile > Emails' do
expect(email).to be_nil
expect(page).to have_content('Email has already been taken')
end
it 'does not add an invalid email' do
fill_in('Email', with: 'test.@example.com')
click_button('Add email address')
email = user.emails.find_by(email: email)
expect(email).to be_nil
expect(page).to have_content('Email is invalid')
end
end
it 'User removes email' do
......
......@@ -5,7 +5,6 @@ require 'spec_helper'
shared_examples_for 'snippet editor' do
before do
stub_feature_flags(snippets_edit_vue: false)
stub_feature_flags(monaco_snippets: flag)
end
def description_field
......@@ -20,7 +19,7 @@ shared_examples_for 'snippet editor' do
fill_in 'project_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do
el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
el = find('.inputarea')
el.send_keys 'Hello World!'
end
end
......@@ -145,15 +144,5 @@ describe 'Projects > Snippets > Create Snippet', :js do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
context 'when using Monaco' do
it_behaves_like "snippet editor" do
let(:flag) { true }
end
end
context 'when using ACE' do
it_behaves_like "snippet editor" do
let(:flag) { false }
end
end
it_behaves_like "snippet editor"
end
......@@ -13,7 +13,6 @@ shared_examples_for 'snippet editor' do
stub_feature_flags(allow_possible_spam: false)
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
stub_feature_flags(monaco_snippets: flag)
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
Gitlab::CurrentSettings.update!(
......@@ -35,7 +34,7 @@ shared_examples_for 'snippet editor' do
find('#personal_snippet_visibility_level_20').set(true)
page.within('.file-editor') do
el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
el = find('.inputarea')
el.send_keys 'Hello World!'
end
end
......@@ -126,15 +125,5 @@ end
describe 'User creates snippet', :js do
let_it_be(:user) { create(:user) }
context 'when using Monaco' do
it_behaves_like "snippet editor" do
let(:flag) { true }
end
end
context 'when using ACE' do
it_behaves_like "snippet editor" do
let(:flag) { false }
end
end
it_behaves_like "snippet editor"
end
......@@ -6,7 +6,6 @@ shared_examples_for 'snippet editor' do
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
stub_feature_flags(monaco_snippets: flag)
sign_in(user)
visit new_snippet_path
end
......@@ -23,7 +22,7 @@ shared_examples_for 'snippet editor' do
fill_in 'personal_snippet_description', with: 'My Snippet **Description**'
page.within('.file-editor') do
el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
el = find('.inputarea')
el.send_keys 'Hello World!'
end
end
......@@ -136,7 +135,7 @@ shared_examples_for 'snippet editor' do
fill_in 'personal_snippet_title', with: 'My Snippet Title'
page.within('.file-editor') do
find(:xpath, "//input[@id='personal_snippet_file_name']").set 'snippet+file+name'
el = flag == true ? find('.inputarea') : find('.ace_text-input', visible: false)
el = find('.inputarea')
el.send_keys 'Hello World!'
end
......@@ -154,15 +153,5 @@ describe 'User creates snippet', :js do
let_it_be(:user) { create(:user) }
context 'when using Monaco' do
it_behaves_like "snippet editor" do
let(:flag) { true }
end
end
context 'when using ACE' do
it_behaves_like "snippet editor" do
let(:flag) { false }
end
end
it_behaves_like "snippet editor"
end
[GitLab API](api/README.md)
......@@ -8,11 +8,6 @@ jest.mock('~/editor/editor_lite', () => {
});
});
const mockCreateAceInstance = jest.fn();
global.ace = {
edit: mockCreateAceInstance,
};
describe('Blob utilities', () => {
beforeEach(() => {
Editor.mockClear();
......@@ -29,21 +24,6 @@ describe('Blob utilities', () => {
});
describe('Monaco editor', () => {
let origProp;
beforeEach(() => {
origProp = window.gon;
window.gon = {
features: {
monacoSnippets: true,
},
};
});
afterEach(() => {
window.gon = origProp;
});
it('initializes the Editor Lite', () => {
utils.initEditorLite({ el: editorEl });
expect(Editor).toHaveBeenCalled();
......@@ -69,27 +49,5 @@ describe('Blob utilities', () => {
]);
});
});
describe('ACE editor', () => {
let origProp;
beforeEach(() => {
origProp = window.gon;
window.gon = {
features: {
monacoSnippets: false,
},
};
});
afterEach(() => {
window.gon = origProp;
});
it('does not initialize the Editor Lite', () => {
utils.initEditorLite({ el: editorEl });
expect(Editor).not.toHaveBeenCalled();
expect(mockCreateAceInstance).toHaveBeenCalledWith(editorEl);
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import PipelineArtifacts from '~/pipelines/components/pipelines_artifacts.vue';
import { GlLink } from '@gitlab/ui';
describe('Pipelines Artifacts dropdown', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(PipelineArtifacts, {
propsData: {
artifacts: [
{
name: 'artifact',
path: '/download/path',
},
{
name: 'artifact two',
path: '/download/path-two',
},
],
},
});
};
const findGlLink = () => wrapper.find(GlLink);
const findAllGlLinks = () => wrapper.find('.dropdown-menu').findAll(GlLink);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('should render a dropdown with all the provided artifacts', () => {
expect(findAllGlLinks()).toHaveLength(2);
});
it('should render a link with the provided path', () => {
expect(findGlLink().attributes('href')).toEqual('/download/path');
expect(findGlLink().text()).toContain('artifact');
});
});
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import axios from '~/lib/utils/axios_utils';
import stage from '~/pipelines/components/stage.vue';
import StageComponent from '~/pipelines/components/stage.vue';
import eventHub from '~/pipelines/event_hub';
import { stageReply } from './mock_data';
import waitForPromises from 'helpers/wait_for_promises';
describe('Pipelines stage component', () => {
let StageComponent;
let component;
let wrapper;
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
StageComponent = Vue.extend(stage);
component = mountComponent(StageComponent, {
stage: {
status: {
group: 'success',
icon: 'status_success',
title: 'success',
},
dropdown_path: 'path.json',
const defaultProps = {
stage: {
status: {
group: 'success',
icon: 'status_success',
title: 'success',
},
dropdown_path: 'path.json',
},
updateDropdown: false,
};
const createComponent = (props = {}) => {
wrapper = mount(StageComponent, {
propsData: {
...defaultProps,
...props,
},
updateDropdown: false,
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
component.$destroy();
wrapper.destroy();
wrapper = null;
mock.restore();
});
it('should render a dropdown with the status icon', () => {
expect(component.$el.getAttribute('class')).toEqual('dropdown');
expect(component.$el.querySelector('svg')).toBeDefined();
expect(component.$el.querySelector('button').getAttribute('data-toggle')).toEqual('dropdown');
describe('default', () => {
beforeEach(() => {
createComponent();
});
it('should render a dropdown with the status icon', () => {
expect(wrapper.attributes('class')).toEqual('dropdown');
expect(wrapper.find('svg').exists()).toBe(true);
expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown');
});
});
describe('with successful request', () => {
beforeEach(() => {
mock.onGet('path.json').reply(200, stageReply);
createComponent();
});
it('should render the received data and emit `clickedDropdown` event', done => {
spyOn(eventHub, '$emit');
component.$el.querySelector('button').click();
it('should render the received data and emit `clickedDropdown` event', () => {
jest.spyOn(eventHub, '$emit');
wrapper.find('button').trigger('click');
setTimeout(() => {
expect(
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
).toContain(stageReply.latest_statuses[0].name);
return waitForPromises().then(() => {
expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
stageReply.latest_statuses[0].name,
);
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
done();
}, 0);
});
});
});
describe('when request fails', () => {
beforeEach(() => {
mock.onGet('path.json').reply(500);
createComponent();
});
it('should close the dropdown', () => {
component.$el.click();
wrapper.setMethods({
closeDropdown: jest.fn(),
isDropdownOpen: jest.fn().mockReturnValue(false),
});
setTimeout(() => {
expect(component.$el.classList.contains('open')).toEqual(false);
}, 0);
wrapper.find('button').trigger('click');
return waitForPromises().then(() => {
expect(wrapper.vm.closeDropdown).toHaveBeenCalled();
});
});
});
......@@ -79,28 +99,30 @@ describe('Pipelines stage component', () => {
const copyStage = Object.assign({}, stageReply);
copyStage.latest_statuses[0].name = 'this is the updated content';
mock.onGet('bar.json').reply(200, copyStage);
});
it('should update the stage to request the new endpoint provided', done => {
component.stage = {
status: {
group: 'running',
icon: 'status_running',
title: 'running',
createComponent({
stage: {
status: {
group: 'running',
icon: 'status_running',
title: 'running',
},
dropdown_path: 'bar.json',
},
dropdown_path: 'bar.json',
};
Vue.nextTick(() => {
component.$el.querySelector('button').click();
});
});
setTimeout(() => {
expect(
component.$el.querySelector('.js-builds-dropdown-container ul').textContent.trim(),
).toContain('this is the updated content');
done();
it('should update the stage to request the new endpoint provided', () => {
return wrapper.vm
.$nextTick()
.then(() => {
wrapper.find('button').trigger('click');
return waitForPromises();
})
.then(() => {
expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
'this is the updated content',
);
});
});
});
});
......@@ -109,27 +131,25 @@ describe('Pipelines stage component', () => {
mock.onGet('path.json').reply(200, stageReply);
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
createComponent({ type: 'PIPELINES_TABLE' });
});
describe('within pipeline table', () => {
it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', done => {
spyOn(eventHub, '$emit');
component.type = 'PIPELINES_TABLE';
component.$el.querySelector('button').click();
setTimeout(() => {
component.$el.querySelector('.js-ci-action').click();
setTimeout(() => {
component
.$nextTick()
.then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
})
.then(done)
.catch(done.fail);
}, 0);
}, 0);
it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', () => {
jest.spyOn(eventHub, '$emit');
wrapper.find('button').trigger('click');
return waitForPromises()
.then(() => {
wrapper.find('.js-ci-action').trigger('click');
return waitForPromises();
})
.then(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
});
});
});
});
......
import { mount } from '@vue/test-utils';
import { GlProgressBar, GlLink, GlBadge, GlDeprecatedButton } from '@gitlab/ui';
import { GlProgressBar, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import ReleaseBlockMilestoneInfo from '~/releases/components/release_block_milestone_info.vue';
import { milestones as originalMilestones } from '../mock_data';
......@@ -106,7 +106,7 @@ describe('Release block milestone info', () => {
const clickShowMoreFewerButton = () => {
milestoneListContainer()
.find(GlDeprecatedButton)
.find(GlButton)
.trigger('click');
return wrapper.vm.$nextTick();
......
import Editor from '~/editor/editor_lite';
import { initEditor } from '~/snippet/snippet_bundle';
import initEditor from '~/snippet/snippet_bundle';
import { setHTMLFixture } from 'helpers/fixtures';
jest.mock('~/editor/editor_lite', () => jest.fn());
describe('Snippet editor', () => {
describe('Monaco editor for Snippets', () => {
let oldGon;
let editorEl;
let contentEl;
let fileNameEl;
let form;
const mockName = 'foo.bar';
const mockContent = 'Foo Bar';
const updatedMockContent = 'New Foo Bar';
const mockEditor = {
createInstance: jest.fn(),
updateModelLanguage: jest.fn(),
getValue: jest.fn().mockReturnValueOnce(updatedMockContent),
};
Editor.mockImplementation(() => mockEditor);
function setUpFixture(name, content) {
setHTMLFixture(`
<div class="snippet-form-holder">
<form>
<input class="js-snippet-file-name" type="text" value="${name}">
<input class="snippet-file-content" type="hidden" value="${content}">
<pre id="editor"></pre>
</form>
</div>
`);
}
function bootstrap(name = '', content = '') {
setUpFixture(name, content);
editorEl = document.getElementById('editor');
contentEl = document.querySelector('.snippet-file-content');
fileNameEl = document.querySelector('.js-snippet-file-name');
form = document.querySelector('.snippet-form-holder form');
initEditor();
}
function createEvent(name) {
return new Event(name, {
view: window,
bubbles: true,
cancelable: true,
});
}
beforeEach(() => {
oldGon = window.gon;
window.gon = { features: { monacoSnippets: true } };
bootstrap(mockName, mockContent);
let editorEl;
let contentEl;
let fileNameEl;
let form;
const mockName = 'foo.bar';
const mockContent = 'Foo Bar';
const updatedMockContent = 'New Foo Bar';
const mockEditor = {
createInstance: jest.fn(),
updateModelLanguage: jest.fn(),
getValue: jest.fn().mockReturnValueOnce(updatedMockContent),
};
Editor.mockImplementation(() => mockEditor);
function setUpFixture(name, content) {
setHTMLFixture(`
<div class="snippet-form-holder">
<form>
<input class="js-snippet-file-name" type="text" value="${name}">
<input class="snippet-file-content" type="hidden" value="${content}">
<pre id="editor"></pre>
</form>
</div>
`);
}
function bootstrap(name = '', content = '') {
setUpFixture(name, content);
editorEl = document.getElementById('editor');
contentEl = document.querySelector('.snippet-file-content');
fileNameEl = document.querySelector('.js-snippet-file-name');
form = document.querySelector('.snippet-form-holder form');
initEditor();
}
function createEvent(name) {
return new Event(name, {
view: window,
bubbles: true,
cancelable: true,
});
}
afterEach(() => {
window.gon = oldGon;
});
beforeEach(() => {
bootstrap(mockName, mockContent);
});
it('correctly initializes Editor', () => {
expect(mockEditor.createInstance).toHaveBeenCalledWith({
el: editorEl,
blobPath: mockName,
blobContent: mockContent,
});
it('correctly initializes Editor', () => {
expect(mockEditor.createInstance).toHaveBeenCalledWith({
el: editorEl,
blobPath: mockName,
blobContent: mockContent,
});
});
it('listens to file name changes and updates syntax highlighting of code', () => {
expect(mockEditor.updateModelLanguage).not.toHaveBeenCalled();
it('listens to file name changes and updates syntax highlighting of code', () => {
expect(mockEditor.updateModelLanguage).not.toHaveBeenCalled();
const event = createEvent('change');
const event = createEvent('change');
fileNameEl.value = updatedMockContent;
fileNameEl.dispatchEvent(event);
fileNameEl.value = updatedMockContent;
fileNameEl.dispatchEvent(event);
expect(mockEditor.updateModelLanguage).toHaveBeenCalledWith(updatedMockContent);
});
expect(mockEditor.updateModelLanguage).toHaveBeenCalledWith(updatedMockContent);
});
it('listens to form submit event and populates the hidden field with most recent version of the content', () => {
expect(contentEl.value).toBe(mockContent);
it('listens to form submit event and populates the hidden field with most recent version of the content', () => {
expect(contentEl.value).toBe(mockContent);
const event = createEvent('submit');
const event = createEvent('submit');
form.dispatchEvent(event);
expect(contentEl.value).toBe(updatedMockContent);
});
form.dispatchEvent(event);
expect(contentEl.value).toBe(updatedMockContent);
});
});
import Vue from 'vue';
import artifactsComp from '~/pipelines/components/pipelines_artifacts.vue';
describe('Pipelines Artifacts dropdown', () => {
let component;
let artifacts;
beforeEach(() => {
const ArtifactsComponent = Vue.extend(artifactsComp);
artifacts = [
{
name: 'artifact',
path: '/download/path',
},
];
component = new ArtifactsComponent({
propsData: {
artifacts,
},
}).$mount();
});
it('should render a dropdown with the provided artifacts', () => {
expect(component.$el.querySelectorAll('.dropdown-menu li').length).toEqual(artifacts.length);
});
it('should render a link with the provided path', () => {
expect(component.$el.querySelector('.dropdown-menu li a').getAttribute('href')).toEqual(
artifacts[0].path,
);
expect(component.$el.querySelector('.dropdown-menu li a').textContent).toContain(
artifacts[0].name,
);
});
});
......@@ -4,7 +4,7 @@ require 'spec_helper'
describe Email do
describe 'validations' do
it_behaves_like 'an object with email-formated attributes', :email do
it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :email do
subject { build(:email) }
end
end
......
......@@ -296,7 +296,7 @@ describe User, :do_not_mock_admin_mode do
subject { build(:user) }
end
it_behaves_like 'an object with email-formated attributes', :public_email, :notification_email do
it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :public_email, :notification_email do
subject { build(:user).tap { |user| user.emails << build(:email, email: email_value) } }
end
......@@ -916,7 +916,6 @@ describe User, :do_not_mock_admin_mode do
user.tap { |u| u.update!(email: new_email) }.reload
end.to change(user, :unconfirmed_email).to(new_email)
end
it 'does not change :notification_email' do
expect do
user.tap { |u| u.update!(email: new_email) }.reload
......
......@@ -44,3 +44,44 @@ RSpec.shared_examples 'an object with email-formated attributes' do |*attributes
end
end
end
RSpec.shared_examples 'an object with RFC3696 compliant email-formated attributes' do |*attributes|
attributes.each do |attribute|
describe "specifically its :#{attribute} attribute" do
%w[
info@example.com
info+test@example.com
o'reilly@example.com
].each do |valid_email|
context "with a value of '#{valid_email}'" do
let(:email_value) { valid_email }
it 'is valid' do
subject.send("#{attribute}=", valid_email)
expect(subject).to be_valid
end
end
end
%w[
foobar
test@test@example.com
test.test.@example.com
.test.test@example.com
mailto:test@example.com
lol!'+=?><#$%^&*()@gmail.com
].each do |invalid_email|
context "with a value of '#{invalid_email}'" do
let(:email_value) { invalid_email }
it 'is invalid' do
subject.send("#{attribute}=", invalid_email)
expect(subject).to be_invalid
end
end
end
end
end
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册