Commit 6d769a8d authored by Robert Speicher's avatar Robert Speicher

Merge branch 'nt/ce-to-ee-thursday' into 'master'

CE upstream: Thursday

See merge request !1497
parents e7098ade b2687717
......@@ -318,6 +318,7 @@ migration paths:
- bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_COMMIT_SHA
- bundle install --without postgres production --jobs $(nproc) $FLAGS --retry=3
- source scripts/prepare_build.sh
- bundle exec rake db:migrate
......
......@@ -479,8 +479,7 @@ merge request:
1. [Rails](https://github.com/bbatsov/rails-style-guide)
1. [Newlines styleguide][newlines-styleguide]
1. [Testing](doc/development/testing.md)
1. [JavaScript (ES6)](https://github.com/airbnb/javascript)
1. [JavaScript (ES5)](https://github.com/airbnb/javascript/tree/es5-deprecated/es5)
1. [JavaScript styleguide][js-styleguide]
1. [SCSS styleguide][scss-styleguide]
1. [Shell commands](doc/development/shell_commands.md) created by GitLab
contributors to enhance security
......@@ -549,7 +548,8 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
[changelog]: doc/development/changelog.md "Generate a changelog entry"
[doc-styleguide]: doc/development/doc_styleguide.md "Documentation styleguide"
[scss-styleguide]: doc/development/scss_styleguide.md "SCSS styleguide"
[js-styleguide]: doc/development/fe_guide/style_guide_js.md "JavaScript styleguide"
[scss-styleguide]: doc/development/fe_guide/style_guide_scss.md "SCSS styleguide"
[newlines-styleguide]: doc/development/newlines_styleguide.md "Newlines styleguide"
[UX Guide for GitLab]: http://docs.gitlab.com/ce/development/ux_guide/
[license-finder-doc]: doc/development/licensing.md
......
......@@ -338,6 +338,7 @@ group :test do
gem 'test_after_commit', '~> 1.1'
gem 'sham_rack', '~> 1.3.6'
gem 'timecop', '~> 0.8.0'
gem 'concurrent-ruby', '~> 1.0.5'
end
gem 'octokit', '~> 4.6.2'
......
......@@ -136,7 +136,7 @@ GEM
execjs
coffee-script-source (1.10.0)
colorize (0.7.7)
concurrent-ruby (1.0.4)
concurrent-ruby (1.0.5)
connection_pool (2.2.1)
crack (0.4.3)
safe_yaml (~> 1.0.0)
......@@ -905,6 +905,7 @@ DEPENDENCIES
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
coffee-rails (~> 4.1.0)
concurrent-ruby (~> 1.0.5)
connection_pool (~> 2.0)
creole (~> 0.5.0)
d3_rails (~> 3.5.0)
......
......@@ -24,7 +24,7 @@
$('body').on('click', '.js-toggle-button', function(e) {
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.target.tagName.toLowerCase();
const targetTag = e.currentTarget.tagName.toLowerCase();
if (targetTag === 'a' || targetTag === 'button') {
e.preventDefault();
}
......
......@@ -48,7 +48,7 @@ import boardCard from './board_card';
this.list.getIssues(false);
}
if (this.scrollHeight() > this.listHeight()) {
if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
......
......@@ -394,7 +394,7 @@ import FilteredSearchContainer from './container';
paths.push(`search=${sanitized}`);
}
const parameterizedUrl = `?scope=all&utf8=&${paths.join('&')}`;
const parameterizedUrl = `?scope=all&utf8=%E2%9C%93&${paths.join('&')}`;
if (this.updateObject) {
this.updateObject(parameterizedUrl);
......
import httpStatusCodes from './http_status';
/**
* Polling utility for handling realtime updates.
* Service for vue resouce and method need to be provided as props
*
* @example
* new poll({
* resource: resource,
* method: 'name',
* data: {page: 1, scope: 'all'},
* successCallback: () => {},
* errorCallback: () => {},
* }).makeRequest();
*
* this.service = new BoardsService(endpoint);
* new poll({
* resource: this.service,
* method: 'get',
* data: {page: 1, scope: 'all'},
* successCallback: () => {},
* errorCallback: () => {},
* }).makeRequest();
*
*
* 1. Checks for response and headers before start polling
* 2. Interval is provided by `Poll-Interval` header.
* 3. If `Poll-Interval` is -1, we stop polling
* 4. If HTTP response is 200, we poll.
* 5. If HTTP response is different from 200, we stop polling.
*
*/
export default class Poll {
constructor(options = {}) {
this.options = options;
this.options.data = options.data || {};
this.intervalHeader = 'POLL-INTERVAL';
}
checkConditions(response) {
const headers = gl.utils.normalizeHeaders(response.headers);
const pollInterval = headers[this.intervalHeader];
if (pollInterval > 0 && response.status === httpStatusCodes.OK) {
this.options.successCallback(response);
setTimeout(() => {
this.makeRequest();
}, pollInterval);
} else {
this.options.successCallback(response);
}
}
makeRequest() {
const { resource, method, data, errorCallback } = this.options;
return resource[method](data)
.then(response => this.checkConditions(response))
.catch(error => errorCallback(error));
}
}
......@@ -33,6 +33,10 @@
};
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode
if ($(e.target).hasClass('js-note-text')) {
$('.js-md-preview-button').focus();
}
return $(document).triggerHandler('markdown-preview:toggle', [e]);
};
......
......@@ -84,7 +84,6 @@ $warning-message-border: #f0e2bb;
*/
$border-color: #e5e5e5;
$focus-border-color: #3aabf0;
$sidebar-collapsed-icon-color: #999;
$well-expand-item: #e8f2f7;
$well-inner-border: #eef0f2;
$well-light-border: #f1f1f1;
......
......@@ -498,7 +498,7 @@
svg {
width: 16px;
height: 16px;
fill: $sidebar-collapsed-icon-color;
fill: $issuable-sidebar-color;
}
&:hover svg {
......
......@@ -46,6 +46,10 @@ ul.related-merge-requests > li {
.merge-request-id {
flex-shrink: 0;
}
.merge-request-info {
margin-left: 5px;
}
}
.merge-requests-title,
......@@ -58,10 +62,6 @@ ul.related-merge-requests > li {
display: inline-block;
}
.merge-request-info {
margin-left: 5px;
}
.merge-request-status {
font-size: 13px;
padding: 0 5px;
......
......@@ -64,8 +64,11 @@ class ApplicationController < ActionController::Base
# This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token!
token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
user = User.find_by_authentication_token(token_string) || User.find_by_personal_access_token(token_string)
token = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
return unless token.present?
user = User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
if user && can?(user, :log_in)
# Notice we are passing store false, so the user is not
......
......@@ -6,7 +6,7 @@ class ChatNotificationService < Service
default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel
boolean_accessor :notify_only_broken_pipelines
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
validates :webhook, presence: true, url: true, if: :activated?
......@@ -17,6 +17,7 @@ class ChatNotificationService < Service
if properties.nil?
self.properties = {}
self.notify_only_broken_pipelines = true
self.notify_only_default_branch = true
end
end
......@@ -29,6 +30,19 @@ class ChatNotificationService < Service
pipeline wiki_page]
end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'checkbox', name: 'notify_only_default_branch' },
]
end
def execute(data)
return unless supported_events.include?(data[:object_kind])
return unless webhook.present?
......@@ -123,6 +137,17 @@ class ChatNotificationService < Service
end
def should_pipeline_be_notified?(data)
notify_for_ref?(data) && notify_for_pipeline?(data)
end
def notify_for_ref?(data)
return true if data[:object_attributes][:tag]
return true unless notify_only_default_branch
data[:object_attributes][:ref] == project.default_branch
end
def notify_for_pipeline?(data)
case data[:object_attributes][:status]
when 'success'
!notify_only_broken_pipelines?
......
......@@ -22,19 +22,11 @@ class MattermostService < ChatNotificationService
</ol>'
end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel_placeholder
"Channel handle (e.g. town-square)"
end
def webhook_placeholder
'http://mattermost.example.com/hooks/…'
end
end
......@@ -21,19 +21,11 @@ class SlackService < ChatNotificationService
</ol>'
end
def fields
default_fields + build_event_channels
end
def default_fields
[
{ type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
end
def default_channel_placeholder
"Channel name (e.g. general)"
end
def webhook_placeholder
'https://hooks.slack.com/services/…'
end
end
......@@ -344,6 +344,8 @@ class User < ActiveRecord::Base
end
def find_by_personal_access_token(token_string)
return unless token_string
PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user
end
......
......@@ -3,7 +3,7 @@ module Boards
class ListService < BaseService
def execute
issues = IssuesFinder.new(current_user, filter_params).execute
issues = without_board_labels(issues) unless movable_list?
issues = without_board_labels(issues) unless list
issues = with_list_label(issues) if movable_list?
issues.order_by_position_and_priority
end
......
......@@ -46,36 +46,44 @@
%h4
Features
%hr
%p
Sign up
- sign_up = "Sign up"
%p{ "aria-label" => "#{sign_up}: status " + (signup_enabled? ? "on" : "off") }
= sign_up
%span.light.pull-right
= boolean_to_icon signup_enabled?
%p
LDAP
- ldap = "LDAP"
%p{ "aria-label" => "#{ldap}: status " + (Gitlab.config.ldap.enabled ? "on" : "off") }
= ldap
%span.light.pull-right
= boolean_to_icon Gitlab.config.ldap.enabled
%p
Gravatar
- gravatar = "Gravatar"
%p{ "aria-label" => "#{gravatar}: status " + (gravatar_enabled? ? "on" : "off") }
= gravatar
%span.light.pull-right
= boolean_to_icon gravatar_enabled?
%p
OmniAuth
- omniauth = "OmniAuth"
%p{ "aria-label" => "#{omniauth}: status " + (Gitlab.config.omniauth.enabled ? "on" : "off") }
= omniauth
%span.light.pull-right
= boolean_to_icon Gitlab.config.omniauth.enabled
%p
Reply by email
- reply_email = "Reply by email"
%p{ "aria-label" => "#{reply_email}: status " + (Gitlab::IncomingEmail.enabled? ? "on" : "off") }
= reply_email
%span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled?
%p
Elasticsearch
- elastic = "Elasticsearch"
%p{ "aria-label" => "#{elastic}: status " + (current_application_settings.elasticsearch_search? ? "on" : "off") }
= elastic
%span.light.pull-right
= boolean_to_icon current_application_settings.elasticsearch_search?
%p
Geo
- geo = "Geo"
%p{ "aria-label" => "#{geo}: status " + (Gitlab::Geo.enabled? ? "on" : "off") }
= geo
%span.light.pull-right
= boolean_to_icon Gitlab::Geo.enabled?
%p
Container Registry
- container_reg = "Container Registry"
%p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") }
= container_reg
%span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled
......
......@@ -19,6 +19,7 @@
.nav-controls
- if @todos.any?(&:pending?)
.append-right-default
= link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete, data: { href: destroy_all_dashboard_todos_path(todos_filter_params) } do
Mark all as done
= icon('spinner spin')
......
......@@ -6,7 +6,9 @@
%h4.prepend-top-0
Syntax highlighting theme
%p
This setting allow you to customize the appearance of the syntax.
This setting allows you to customize the appearance of the syntax.
= succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'syntax-highlighting-theme'), target: '_blank'
.col-lg-9.syntax-theme
- Gitlab::ColorSchemes.each do |scheme|
= label_tag do
......@@ -20,6 +22,8 @@
Behavior
%p
This setting allows you to customize the behavior of the system layout and default views.
= succeed '.' do
= link_to 'Learn more', help_page_path('user/profile/preferences', anchor: 'behavior'), target: '_blank'
.col-lg-9
.form-group
= f.label :layout, class: 'label-light' do
......@@ -29,13 +33,11 @@
Choose between fixed (max. 1200px) and fluid (100%) application layout.
.form-group
= f.label :dashboard, class: 'label-light' do
Default Dashboard
= link_to('(?)', help_page_path('profile/preferences') + '#default-dashboard', target: '_blank')
Default dashboard
= f.select :dashboard, dashboard_choices, {}, class: 'form-control'
.form-group
= f.label :project_view, class: 'label-light' do
Project view
= link_to('(?)', help_page_path('profile/preferences') + '#default-project-view', target: '_blank')
= f.select :project_view, project_view_choices, {}, class: 'form-control'
.help-block
Choose what content you want to see on a project's home page.
......
......@@ -40,7 +40,7 @@
.form-group
= f.label :tag_list, "Tags", class: 'label-light'
= f.text_field :tag_list, value: @project.tag_list.to_s, maxlength: 2000, class: "form-control"
= f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control"
%p.help-block Separate tags with commas.
%hr
%fieldset
......
......@@ -18,7 +18,7 @@
%th Last edit
- @services.sort_by(&:title).each do |service|
%tr
%td
%td{ "aria-label" => "#{service.title}: status " + (service.activated? ? "on" : "off") }
= boolean_to_icon service.activated?
%td
= link_to edit_namespace_project_service_path(@project.namespace, @project, service.to_param) do
......
---
title: Display all closed issues in “done” board list
merge_request:
author:
---
title: Add `aria-label` for feature status accessibility
merge_request: 9830
author:
---
title: Adds Frontend Styleguide to documentation
merge_request: 9961
author:
---
title: Fix quick submit short-cut on preview tab for comments
merge_request: 10002
author:
---
title: align Mark all as done with other Done buttons on Todos page
merge_request:
author:
---
title: Adds polling utility function for vue resource
merge_request:
author:
---
title: Fixed filtered search not working in IE
merge_request:
author:
---
title: Fixed bug in issue boards which stopped cards being able to be dragged
merge_request:
author:
---
title: Corrected time tracking icon color in the issuable side bar
merge_request:
author:
---
title: Only send chat notifications for the default branch
merge_request:
author:
Rails.application.configure do
# Make sure the middleware is inserted first in middleware chain
config.middleware.insert_before('ActionDispatch::Static', 'Gitlab::Testing::RequestBlockerMiddleware')
# Settings specified here will take precedence over those in config/application.rb
# The test environment is used exclusively to run your application's
......
......@@ -42,9 +42,9 @@ GET /projects/:id/triggers/:trigger_id
```
| Attribute | Type | required | Description |
|-----------|---------|----------|--------------------------|
|--------------|---------|----------|--------------------------|
| `id` | integer | yes | The ID of a project |
| `token` | string | yes | The `token` of a trigger |
| `trigger_id` | integer | yes | The trigger id |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/triggers/5"
......@@ -103,6 +103,7 @@ PUT /projects/:id/triggers/:trigger_id
| Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------|
| `id` | integer | yes | The ID of a project |
| `trigger_id` | integer | yes | The trigger id |
| `description` | string | no | The trigger name |
......@@ -133,6 +134,7 @@ POST /projects/:id/triggers/:trigger_id/take_ownership
| Attribute | Type | required | Description |
|---------------|---------|----------|--------------------------|
| `id` | integer | yes | The ID of a project |
| `trigger_id` | integer | yes | The trigger id |
```
......
......@@ -15,7 +15,7 @@
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md)
- [UX guide](ux_guide/index.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](frontend.md)
- [Frontend guidelines](fe_guide/index.md)
- [SQL guidelines](sql.md) for working with SQL queries
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
- [`Gemfile` guidelines](gemfile.md)
......
# Accessibility
## Resources
[Chrome Accessibility Developer Tools][chrome-accessibility-developer-tools]
are useful for testing for potential accessibility problems in GitLab.
Accessibility best-practices and more in-depth information is available on
[the Audit Rules page][audit-rules] for the Chrome Accessibility Developer Tools.
[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
# Architecture
When you are developing a new feature that requires architectural design, or if
you are changing the fundamental design of an existing feature, make sure it is
discussed with one of the Frontend Architecture Experts.
A Frontend Architect is an expert who makes high-level Frontend design decisions
and decides on technical standards, including coding standards and frameworks.
Architectural decisions should be accessible to everyone, so please document
them in the relevant Merge Request discussion or by updating our documentation
when appropriate.
You can find the Frontend Architecture experts on the [team page][team-page].
## Examples
You can find documentation about the desired architecture for a new feature
built with Vue.js [here][vue-section].
[team-page]: https://about.gitlab.com/team
[vue-section]: vue.md#frontend.html#how-to-build-a-new-feature-with-vue-js
# Design Patterns
## Singletons
When exactly one object is needed for a given task, prefer to define it as a
`class` rather than as an object literal. Prefer also to explicitly restrict
instantiation, unless flexibility is important (e.g. for testing).
```javascript
// bad
const MyThing = {
prop1: 'hello',
method1: () => {}
};
export default MyThing;
// good
class MyThing {
constructor() {
this.prop1 = 'hello';
}
method1() {}
}
export default new MyThing();
// best
export default class MyThing {
constructor() {
if (!this.prototype.singleton) {
this.init();
this.prototype.singleton = this;
}
return this.prototype.singleton;
}
init() {
this.prop1 = 'hello';
}
method1() {}
}
```
## Manipulating the DOM in a JS Class
When writing a class that needs to manipulate the DOM guarantee a container option is provided.
This is useful when we need that class to be instantiated more than once in the same page.
Bad:
```javascript
class Foo {
constructor() {
document.querySelector('.bar');
}
}
new Foo();
```
Good:
```javascript
class Foo {
constructor(opts) {
document.querySelector(`${opts.container} .bar`);
}
}
new Foo({ container: '.my-element' });
```
You can find an example of the above in this [class][container-class-example];
[container-class-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/mini_pipeline_graph_dropdown.js
# Frontend Development Guidelines
This document describes various guidelines to ensure consistency and quality
across GitLab's frontend team.
## Overview
GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with
[Hamlit][hamlit]. Be wary of [the limitations that come with using
Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
modern ECMAScript standards supported through [Babel][babel] and ES module
support through [webpack][webpack].
We also utilize [webpack][webpack] to handle the bundling, minification, and
compression of our assets.
Working with our frontend assets requires Node (v4.3 or greater) and Yarn
(v0.17 or greater). You can find information on how to install these on our
[installation guide][install].
[jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements.
### Browser Support
For our currently-supported browsers, see our [requirements][requirements].
---
## [Architecture](architecture.md)
How we go about making fundamental design decisions in GitLab's frontend team
or make changes to our frontend development guidelines.
---
## [Testing](testing.md)
How we write frontend tests, run the GitLab test suite, and debug test related
issues.
---
## [Design Patterns](design_patterns.md)
Common JavaScript design patterns in GitLab's codebase.
---
## [Vue.js Best Practices](vue.md)
Vue specific design patterns and practices.
---
## Style Guides
### [JavaScript Style Guide](style_guide_js.md)
We use eslint to enforce our JavaScript style guides. Our guide is based on
the excellent [Airbnb][airbnb-js-style-guide] style guide with a few small
changes.
### [SCSS Style Guide](style_guide_scss.md)
Our SCSS conventions which are enforced through [scss-lint][scss-lint].
---
## [Performance](performance.md)
Best practices for monitoring and maximizing frontend performance.
---
## [Security](security.md)
Frontend security practices.
---
## [Accessibility](accessibility.md)
Our accessibility standards and resources.
[rails]: http://rubyonrails.org/
[haml]: http://haml.info/
[hamlit]: https://github.com/k0kubun/hamlit
[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
[scss]: http://sass-lang.com/
[babel]: https://babeljs.io/
[webpack]: https://webpack.js.org/
[jquery]: https://jquery.com/
[vue]: http://vuejs.org/
[airbnb-js-style-guide]: https://github.com/airbnb/javascript
[scss-lint]: https://github.com/brigade/scss-lint
[install]: ../../install/installation.md#4-node
[requirements]: ../../install/requirements.md#supported-web-browsers
# Performance
## Best Practices
### Realtime Components
When writing code for realtime features we have to keep a couple of things in mind:
1. Do not overload the server with requests.
1. It should feel realtime.
Thus, we must strike a balance between sending requests and the feeling of realtime.
Use the following rules when creating realtime solutions.
1. The server will tell you how much to poll by sending `Poll-Interval` in the header.
Use that as your polling interval. This way it is easy for system administrators to change the
polling rate.
A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
1. A response with HTTP status `4XX` or `5XX` should disable polling as well.
1. Use a common library for polling.
1. Poll on active tabs only. Use a common library to find out which tab currently has eyes on it.
Please use [Focus](https://gitlab.com/andrewn/focus). Specifically [Eyeballs Detector](https://gitlab.com/andrewn/focus/blob/master/lib/eyeballs-detector.js).
1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be
controlled by the server.
1. The backend code will most likely be using etags. You do not and should not check for status
`304 Not Modified`. The browser will transform it for you.
## Reducing Asset Footprint
### Page-specific JavaScript
Certain pages may require the use of a third party library, such as [d3][d3] for
the User Activity Calendar and [Chart.js][chartjs] for the Graphs pages. These
libraries increase the page size significantly, and impact load times due to
bandwidth bottlenecks and the browser needing to parse more JavaScript.
In cases where libraries are only used on a few specific pages, we use
"page-specific JavaScript" to prevent the main `main.js` file from
becoming unnecessarily large.
Steps to split page-specific JavaScript from the main `main.js`:
1. Create a directory for the specific page(s), e.g. `graphs/`.
1. In that directory, create a `namespace_bundle.js` file, e.g. `graphs_bundle.js`.
1. Add the new "bundle" file to the list of entry files in `config/webpack.config.js`.
- For example: `graphs: './graphs/graphs_bundle.js',`.
1. Move code reliant on these libraries into the `graphs` directory.
1. In `graphs_bundle.js` add CommonJS `require('./path_to_some_component.js');` statements to load any other files in this directory. Make sure to use relative urls.
1. In the relevant views, add the scripts to the page with the following:
```haml
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('lib_chart')
= page_specific_javascript_bundle_tag('graphs')
```
The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js`
is separated from the bundle file so it can be cached separately from the bundle
and reused for other pages that also rely on the library. For an example, see
[this Haml file][page-specific-js-example].
### Code Splitting
> *TODO* flesh out this section once webpack is ready for code-splitting
### Minimizing page size
A smaller page size means the page loads faster (especially important on mobile
and poor connections), the page is parsed more quickly by the browser, and less
data is used for users with capped data plans.
General tips:
- Don't add new fonts.
- Prefer font formats with better compression, e.g. WOFF2 is better than WOFF, which is better than TTF.
- Compress and minify assets wherever possible (For CSS/JS, Sprockets and webpack do this for us).
- If some functionality can reasonably be achieved without adding extra libraries, avoid them.
- Use page-specific JavaScript as described above to dynamically load libraries that are only needed on certain pages.
-------
## Additional Resources
- [WebPage Test][web-page-test] for testing site loading time and size.
- [Google PageSpeed Insights][pagespeed-insights] grades web pages and provides feedback to improve the page.
- [Profiling with Chrome DevTools][google-devtools-profiling]
- [Browser Diet][browser-diet] is a community-built guide that catalogues practical tips for improving web page performance.
[web-page-test]: http://www.webpagetest.org/
[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
[browser-diet]: https://browserdiet.com/
[d3]: https://d3js.org/
[chartjs]: http://www.chartjs.org/
[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
# Security
### Resources
[Mozilla’s HTTP Observatory CLI][observatory-cli] and the
[Qualys SSL Labs Server Test][qualys-ssl] are good resources for finding
potential problems and ensuring compliance with security best practices.
<!-- Uncomment these sections when CSP/SRI are implemented.
### Content Security Policy (CSP)
Content Security Policy is a web standard that intends to mitigate certain
forms of Cross-Site Scripting (XSS) as well as data injection.
Content Security Policy rules should be taken into consideration when
implementing new features, especially those that may rely on connection with
external services.
GitLab's CSP is used for the following:
- Blocking plugins like Flash and Silverlight from running at all on our pages.
- Blocking the use of scripts and stylesheets downloaded from external sources.
- Upgrading `http` requests to `https` when possible.
- Preventing `iframe` elements from loading in most contexts.
Some exceptions include:
- Scripts from Google Analytics and Piwik if either is enabled.
- Connecting with GitHub, Bitbucket, GitLab.com, etc. to allow project importing.
- Connecting with Google, Twitter, GitHub, etc. to allow OAuth authentication.
We use [the Secure Headers gem][secure_headers] to enable Content
Security Policy headers in the GitLab Rails app.
Some resources on implementing Content Security Policy:
- [MDN Article on CSP][mdn-csp]
- [GitHub’s CSP Journey on the GitHub Engineering Blog][github-eng-csp]
- The Dropbox Engineering Blog's series on CSP: [1][dropbox-csp-1], [2][dropbox-csp-2], [3][dropbox-csp-3], [4][dropbox-csp-4]
### Subresource Integrity (SRI)
Subresource Integrity prevents malicious assets from being provided by a CDN by
guaranteeing that the asset downloaded is identical to the asset the server
is expecting.
The Rails app generates a unique hash of the asset, which is used as the
asset's `integrity` attribute. The browser generates the hash of the asset
on-load and will reject the asset if the hashes do not match.
All CSS and JavaScript assets should use Subresource Integrity.
Some resources on implementing Subresource Integrity:
- [MDN Article on SRI][mdn-sri]
- [Subresource Integrity on the GitHub Engineering Blog][github-eng-sri]
-->
### Including external resources
External fonts, CSS, and JavaScript should never be used with the exception of
Google Analytics and Piwik - and only when the instance has enabled it. Assets
should always be hosted and served locally from the GitLab instance. Embedded
resources via `iframes` should never be used except in certain circumstances
such as with ReCaptcha, which cannot be used without an `iframe`.
### Avoiding inline scripts and styles
In order to protect users from [XSS vulnerabilities][xss], we will disable
inline scripts in the future using Content Security Policy.
While inline scripts can be useful, they're also a security concern. If
user-supplied content is unintentionally left un-sanitized, malicious users can
inject scripts into the web app.
Inline styles should be avoided in almost all cases, they should only be used
when no alternatives can be found. This allows reusability of styles as well as
readability.
[observatory-cli]: https://github.com/mozilla/http-observatory-cli
[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
[secure_headers]: https://github.com/twitter/secureheaders
[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
[github-eng-sri]: http://githubengineering.com/subresource-integrity/
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
# Style guides and linting
See the relevant style guides for our guidelines and for information on linting:
## JavaScript
We defer to [Airbnb][airbnb-js-style-guide] on most style-related
conventions and enforce them with eslint.
See [our current .eslintrc][eslintrc] for specific rules and patterns.
### Common
#### ESlint
- **Never** disable eslint rules unless you have a good reason. You may see a lot of legacy files with `/* eslint-disable some-rule, some-other-rule */` at the top, but legacy files are a special case. Any time you develop a new feature or refactor an existing one, you should abide by the eslint rules.
- **Never Ever EVER** disable eslint globally for a file
```javascript
// bad
/* eslint-disable */
// better
/* eslint-disable some-rule, some-other-rule */
// best
// nothing :)
```
- If you do need to disable a rule for a single violation, try to do it as locally as possible
```javascript
// bad
/* eslint-disable no-new */
import Foo from 'foo';
new Foo();
// better
import Foo from 'foo';
// eslint-disable-next-line no-new
new Foo();
```
- When they are needed _always_ place ESlint directive comment blocks on the first line of a script, followed by any global declarations, then a blank newline prior to any imports or code.
```javascript
// bad
/* global Foo */
/* eslint-disable no-new */
import Bar from './bar';
// good
/* eslint-disable no-new */
/* global Foo */
import Bar from './bar';
```
- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
- When declaring multiple globals, always use one `/* global [name] */` line per variable.
```javascript
// bad
/* globals Flash, Cookies, jQuery */
// good
/* global Flash */
/* global Cookies */
/* global jQuery */
```
#### Modules, Imports, and Exports
- Use ES module syntax to import modules
```javascript
// bad
require('foo');
// good
import Foo from 'foo';
// bad
module.exports = Foo;
// good
export default Foo;
```
- Relative paths
Unless you are writing a test, always reference other scripts using relative paths instead of `~`
In **app/assets/javascripts**:
```javascript
// bad
import Foo from '~/foo'
// good
import Foo from '../foo';
```
In **spec/javascripts**:
```javascript
// bad
import Foo from '../../app/assets/javascripts/foo'
// good
import Foo from '~/foo';
```
- Avoid using IIFE. Although we have a lot of examples of files which wrap their contents in IIFEs (immediately-invoked function expressions), this is no longer necessary after the transition from Sprockets to webpack. Do not use them anymore and feel free to remove them when refactoring legacy code.
- Avoid adding to the global namespace.
```javascript
// bad
window.MyClass = class { /* ... */ };
// good
export default class MyClass { /* ... */ }
```
- Side effects are forbidden in any script which contains exports
```javascript
// bad
export default class MyClass { /* ... */ }
document.addEventListener("DOMContentLoaded", function(event) {
new MyClass();
}
```
#### Data Mutation and Pure functions
- Strive to write many small pure functions, and minimize where mutations occur.
```javascript
// bad
const values = {foo: 1};
function impureFunction(items) {
const bar = 1;
items.foo = items.a * bar + 2;
return items.a;
}
const c = impureFunction(values);
// good
var values = {foo: 1};
function pureFunction (foo) {
var bar = 1;
foo = foo * bar + 2;
return foo;
}
var c = pureFunction(values.foo);
```
- Avoid constructors with side-effects
#### Parse Strings into Numbers
- `parseInt()` is preferable over `Number()` or `+`
```javascript
// bad
+'10' // 10
// good
Number('10') // 10
// better
parseInt('10', 10);
```
### Vue.js
#### Basic Rules
- Only include one Vue.js component per file.
- Export components as plain objects:
```javascript
export default {
template: `<h1>I'm a component</h1>
}
```
#### Naming
- **Extensions**: Use `.vue` extension for Vue components.
- **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances:
```javascript
// bad
import cardBoard from 'cardBoard';
// good
import CardBoard from 'cardBoard'
// bad
components: {
CardBoard: CardBoard
};
// good
components: {
cardBoard: CardBoard
};
```
- **Props Naming**: Avoid using DOM component prop names.
```javascript
// bad
<component class="btn">
// good
<component cssClass="btn">
```
#### Alignment
- Follow these alignment styles for the template method:
```javascript
// bad
<component v-if="bar"
param="baz" />
// good
<component
v-if="bar"
param="baz"
/>
// if props fit in one line then keep it on the same line
<component bar="bar" />
```
#### Quotes
- Always use double quotes `"` inside templates and single quotes `'` for all other JS.
```javascript
// bad
template: `
<button :class='style'>Button</button>
`
// good
template: `
<button :class="style">Button</button>
`
```
#### Props
- Props should be declared as an object
```javascript
// bad
props: ['foo']
// good
props: {
foo: {
type: String,
required: false,
default: 'bar'
}
}
```
- Required key should always be provided when declaring a prop
```javascript
// bad
props: {
foo: {
type: String,
}
}
// good
props: {
foo: {
type: String,
required: false,
default: 'bar'
}
}
```
- Default key should always be provided if the prop is not required:
```javascript
// bad
props: {
foo: {
type: String,
required: false,
}
}
// good
props: {
foo: {
type: String,
required: false,
default: 'bar'
}
}
// good
props: {
foo: {
type: String,
required: true
}
}
```
#### Data
- `data` method should always be a function
```javascript
// bad
data: {
foo: 'foo'
}
// good
data() {
return {
foo: 'foo'
};
}
```
#### Directives
- Shorthand `@` is preferable over `v-on`
```javascript
// bad
<component v-on:click="eventHandler"/>
// good
<component @click="eventHandler"/>
```
- Shorthand `:` is preferable over `v-bind`
```javascript
// bad
<component v-bind:class="btn"/>
// good
<component :class="btn"/>
```
#### Closing tags
- Prefer self closing component tags
```javascript
// bad
<component></component>
// good
<component />
```
#### Ordering
- Order for a Vue Component:
1. `name`
2. `props`
3. `data`
4. `components`
5. `computedProps`
6. `methods`
7. lifecycle methods
1. `beforeCreate`
2. `created`
3. `beforeMount`
4. `mounted`
5. `beforeUpdate`
6. `updated`
7. `activated`
8. `deactivated`
9. `beforeDestroy`
10. `destroyed`
8. `template`
## SCSS
- [SCSS](style_guide_scss.md)
[airbnb-js-style-guide]: https://github.com/airbnb/javascript
[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc
......@@ -165,6 +165,26 @@ Do not use any selector prefixed with `js-` for styling purposes. These
selectors are intended for use only with JavaScript to allow for removal or
renaming without breaking styling.
### IDs
Don't use ID selectors in CSS.
```scss
// Bad
#my-element {
padding: 0;
}
// Good
.my-element {
padding: 0;
}
```
### Variables
Before adding a new variable for a color or a size, guarantee:
1. There isn't already one
2. There isn't a similar one we can use instead.
## Linting
We use [SCSS Lint][scss-lint] to check for style guide conformity. It uses the
......
# Frontend Testing
There are two types of tests you'll encounter while developing frontend code
at GitLab. We use Karma and Jasmine for JavaScript unit testing, and RSpec
feature tests with Capybara for integration testing.
Feature tests need to be written for all new features. Regression tests ought
to be written for all bug fixes to prevent them from recurring in the future.
See [the Testing Standards and Style Guidelines](/doc/development/testing.md)
for more information on general testing practices at GitLab.
## Karma test suite
GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test
framework for our JavaScript unit tests. For tests that rely on DOM
manipulation we use fixtures which are pre-compiled from HAML source files and
served during testing by the [jasmine-jquery][jasmine-jquery] plugin.
### Running frontend tests
`rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks:
- `rake karma:fixtures` (re-)generates fixtures
- `rake karma:tests` actually executes the tests
As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`)
is sufficient (and saves you some time).
### Live testing and focused testing
While developing locally, it may be helpful to keep karma running so that you
can get instant feedback on as you write tests and modify code. To do this
you can start karma with `npm run karma-start`. It will compile the javascript
assets and run a server at `http://localhost:9876/` where it will automatically
run the tests on any browser which connects to it. You can enter that url on
multiple browsers at once to have it run the tests on each in parallel.
While karma is running, any changes you make will instantly trigger a recompile
and retest of the entire test suite, so you can see instantly if you've broken
a test with your changes. You can use [jasmine focused][jasmine-focus] or
excluded tests (with `fdescribe` or `xdescribe`) to get karma to run only the
tests you want while you're working on a specific feature, but make sure to
remove these directives when you commit your code.
## RSpec Feature Integration Tests
Information on setting up and running RSpec integration tests with
[Capybara][capybara] can be found in the
[general testing guide](/doc/development/testing.md).
## Gotchas
### Errors due to use of unsupported JavaScript features
Similar errors will be thrown if you're using JavaScript features not yet
supported by the PhantomJS test runner which is used for both Karma and RSpec
tests. We polyfill some JavaScript objects for older browsers, but some
features are still unavailable:
- Array.from
- Array.first
- Async functions
- Generators
- Array destructuring
- For..Of
- Symbol/Symbol.iterator
- Spread
Until these are polyfilled appropriately, they should not be used. Please
update this list with additional unsupported features.
### RSpec errors due to JavaScript
By default RSpec unit tests will not run JavaScript in the headless browser
and will simply rely on inspecting the HTML generated by rails.
If an integration test depends on JavaScript to run correctly, you need to make
sure the spec is configured to enable JavaScript when the tests are run. If you
don't do this you'll see vague error messages from the spec runner.
To enable a JavaScript driver in an `rspec` test, add `js: true` to the
individual spec or the context block containing multiple specs that need
JavaScript enabled:
```ruby
# For one spec
it 'presents information about abuse report', js: true do
# assertions...
end
describe "Admin::AbuseReports", js: true do
it 'presents information about abuse report' do
# assertions...
end
it 'shows buttons for adding to abuse report' do
# assertions...
end
end
```
### Spinach errors due to missing JavaScript
> **Note:** Since we are discouraging the use of Spinach when writing new
> feature tests, you shouldn't ever need to use this. This information is kept
> available for legacy purposes only.
In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
file for the failing spec, add the `@javascript` flag above the Scenario:
```
@javascript
Scenario: Developer can approve merge request
Given I am a "Shop" developer
And I visit project "Shop" merge requests page
And merge request 'Bug NS-04' must be approved
And I click link "Bug NS-04"
When I click link "Approve"
Then I should see approved merge request "Bug NS-04"
```
[capybara]: http://teamcapybara.github.io/capybara/
[jasmine]: https://jasmine.github.io/
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/
# Vue
For more complex frontend features, we recommend using Vue.js. It shares
some ideas with React.js as well as Angular.
To get started with Vue, read through [their documentation][vue-docs].
## When to use Vue.js
We recommend using Vue for more complex features. Here are some guidelines for when to use Vue.js:
- If you are starting a new feature or refactoring an old one that highly interacts with the DOM;
- For real time data updates;
- If you are creating a component that will be reused elsewhere;
## When not to use Vue.js
We don't want to refactor all GitLab frontend code into Vue.js, here are some guidelines for
when not to use Vue.js:
- Adding or changing static information;
- Features that highly depend on jQuery will be hard to work with Vue.js
As always, the Frontend Architectural Experts are available to help with any Vue or JavaScript questions.
## How to build a new feature with Vue.js
**Components, Stores and Services**
In some features implemented with Vue.js, like the [issue board][issue-boards]
or [environments table][environments-table]
you can find a clear separation of concerns:
```
new_feature
├── components
│ └── component.js.es6
│ └── ...
├── store
│ └── new_feature_store.js.es6
├── service
│ └── new_feature_service.js.es6
├── new_feature_bundle.js.es6
```
_For consistency purposes, we recommend you to follow the same structure._
Let's look into each of them:
**A `*_bundle.js` file**
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
The Store and the Service should be imported and initialized in this file and provided as a prop to the main component.
Don't forget to follow [these steps.][page_specific_javascript]
**A folder for Components**
This folder holds all components that are specific of this new feature.
If you need to use or create a component that will probably be used somewhere
else, please refer to `vue_shared/components`.
A good thumb rule to know when you should create a component is to think if
it will be reusable elsewhere.
For example, tables are used in a quite amount of places across GitLab, a table
would be a good fit for a component. On the other hand, a table cell used only
in one table would not be a good use of this pattern.
You can read more about components in Vue.js site, [Component System][component-system]
**A folder for the Store**
The Store is a class that allows us to manage the state in a single
source of truth.
The concept we are trying to follow is better explained by Vue documentation
itself, please read this guide: [State Management][state-management]
**A folder for the Service**
The Service is used only to communicate with the server.
It does not store or manipulate any data.
We use [vue-resource][vue-resource-repo] to
communicate with the server.
The [issue boards service][issue-boards-service]
is a good example of this pattern.
## Style guide
Please refer to the Vue section of our [style guide](style_guide_js.md#vuejs)
for best practices while writing your Vue components and templates.
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[vue-resource-repo]: https://github.com/pagekit/vue-resource
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
# Frontend Development Guidelines
This document describes various guidelines to ensure consistency and quality
across GitLab's frontend team.
## Overview
GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with
[Hamlit][hamlit]. Be wary of [the limitations that come with using
Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
[ES6 by way of Babel][es6].
The asset pipeline is [Sprockets][sprockets], which handles the concatenation,
minification, and compression of our assets.
[jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements.
### Architecture
The Frontend Architect is an expert who makes high-level frontend design choices
and decides on technical standards, including coding standards, and frameworks.
When you are assigned a new feature that requires architectural design,
make sure it is discussed with one of the Frontend Architecture Experts.
This rule also applies if you plan to change the architecture of an existing feature.
These decisions should be accessible to everyone, so please document it on the Merge Request.
You can find the Frontend Architecture experts on the [team page][team-page].
You can find documentation about the desired architecture for a new feature built with Vue.js in [here][vue-section].
### Realtime
When writing code for realtime features we have to keep a couple of things in mind:
1. Do not overload the server with requests.
1. It should feel realtime.
Thus, we must strike a balance between sending requests and the feeling of realtime.
Use the following rules when creating realtime solutions.
1. The server will tell you how much to poll by sending `Poll-Interval` in the header.
Use that as your polling interval. This way it is easy for system administrators to change the
polling rate.
A `Poll-Interval: -1` means you should disable polling, and this must be implemented.
1. A response with HTTP status `4XX` or `5XX` should disable polling as well.
1. Use a common library for polling.
1. Poll on active tabs only. Use a common library to find out which tab currently has eyes on it.
Please use [Focus](https://gitlab.com/andrewn/focus). Specifically [Eyeballs Detector](https://gitlab.com/andrewn/focus/blob/master/lib/eyeballs-detector.js).
1. Use regular polling intervals, do not use backoff polling, or jitter, as the interval will be
controlled by the server.
1. The backend code will most likely be using etags. You do not and should not check for status
`304 Not Modified`. The browser will transform it for you.
### Vue
For more complex frontend features, we recommend using Vue.js. It shares
some ideas with React.js as well as Angular.
To get started with Vue, read through [their documentation][vue-docs].
#### How to build a new feature with Vue.js
**Components, Stores and Services**
In some features implemented with Vue.js, like the [issue board][issue-boards]
or [environments table][environments-table]
you can find a clear separation of concerns:
```
new_feature
├── components
│ └── component.js.es6
│ └── ...
├── store
│ └── new_feature_store.js.es6
├── service
│ └── new_feature_service.js.es6
├── new_feature_bundle.js.es6
```
_For consistency purposes, we recommend you to follow the same structure._
Let's look into each of them:
**A `*_bundle.js` file**
This is the index file of your new feature. This is where the root Vue instance
of the new feature should be.
The Store and the Service should be imported and initialized in this file and provided as a prop to the main component.
Don't forget to follow [these steps.][page_specific_javascript]
**A folder for Components**
This folder holds all components that are specific of this new feature.
If you need to use or create a component that will probably be used somewhere
else, please refer to `vue_shared/components`.
A good thumb rule to know when you should create a component is to think if
it will be reusable elsewhere.
For example, tables are used in a quite amount of places across GitLab, a table
would be a good fit for a component.
On the other hand, a table cell used only in on table, would not be a good use
of this pattern.
You can read more about components in Vue.js site, [Component System][component-system]
**A folder for the Store**
The Store is a class that allows us to manage the state in a single
source of truth.
The concept we are trying to follow is better explained by Vue documentation
itself, please read this guide: [State Management][state-management]
**A folder for the Service**
The Service is used only to communicate with the server.
It does not store or manipulate any data.
We use [vue-resource][vue-resource-repo] to
communicate with the server.
The [issue boards service][issue-boards-service]
is a good example of this pattern.
## Performance
### Resources
- [WebPage Test][web-page-test] for testing site loading time and size.
- [Google PageSpeed Insights][pagespeed-insights] grades web pages and provides feedback to improve the page.
- [Profiling with Chrome DevTools][google-devtools-profiling]
- [Browser Diet][browser-diet] is a community-built guide that catalogues practical tips for improving web page performance.
### Page-specific JavaScript
Certain pages may require the use of a third party library, such as [d3][d3] for
the User Activity Calendar and [Chart.js][chartjs] for the Graphs pages. These
libraries increase the page size significantly, and impact load times due to
bandwidth bottlenecks and the browser needing to parse more JavaScript.
In cases where libraries are only used on a few specific pages, we use
"page-specific JavaScript" to prevent the main `application.js` file from
becoming unnecessarily large.
Steps to split page-specific JavaScript from the main `application.js`:
1. Create a directory for the specific page(s), e.g. `graphs/`.
1. In that directory, create a `namespace_bundle.js` file, e.g. `graphs_bundle.js`.
1. In `graphs_bundle.js` add the line `//= require_tree .`, this adds all other files in the directory to the bundle.
1. Add any necessary libraries to `app/assets/javascripts/lib/`, all files directly descendant from this directory will be precompiled as separate assets, in this case `chart.js` would be added.
1. Add the new "bundle" file to the list of precompiled assets in
`config/application.rb`.
- For example: `config.assets.precompile << "graphs/graphs_bundle.js"`.
1. Move code reliant on these libraries into the `graphs` directory.
1. In the relevant views, add the scripts to the page with the following:
```haml
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/chart.js')
= page_specific_javascript_tag('graphs/graphs_bundle.js')
```
The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js`
is separated from the bundle file so it can be cached separately from the bundle
and reused for other pages that also rely on the library. For an example, see
[this Haml file][page-specific-js-example].
### Minimizing page size
A smaller page size means the page loads faster (especially important on mobile
and poor connections), the page is parsed more quickly by the browser, and less
data is used for users with capped data plans.
General tips:
- Don't add new fonts.
- Prefer font formats with better compression, e.g. WOFF2 is better than WOFF, which is better than TTF.
- Compress and minify assets wherever possible (For CSS/JS, Sprockets does this for us).
- If some functionality can reasonably be achieved without adding extra libraries, avoid them.
- Use page-specific JavaScript as described above to dynamically load libraries that are only needed on certain pages.
## Accessibility
### Resources
[Chrome Accessibility Developer Tools][chrome-accessibility-developer-tools]
are useful for testing for potential accessibility problems in GitLab.
Accessibility best-practices and more in-depth information is available on
[the Audit Rules page][audit-rules] for the Chrome Accessibility Developer Tools.
## Security
### Resources
[Mozilla’s HTTP Observatory CLI][observatory-cli] and the
[Qualys SSL Labs Server Test][qualys-ssl] are good resources for finding
potential problems and ensuring compliance with security best practices.
<!-- Uncomment these sections when CSP/SRI are implemented.
### Content Security Policy (CSP)
Content Security Policy is a web standard that intends to mitigate certain
forms of Cross-Site Scripting (XSS) as well as data injection.
Content Security Policy rules should be taken into consideration when
implementing new features, especially those that may rely on connection with
external services.
GitLab's CSP is used for the following:
- Blocking plugins like Flash and Silverlight from running at all on our pages.
- Blocking the use of scripts and stylesheets downloaded from external sources.
- Upgrading `http` requests to `https` when possible.
- Preventing `iframe` elements from loading in most contexts.
Some exceptions include:
- Scripts from Google Analytics and Piwik if either is enabled.
- Connecting with GitHub, Bitbucket, GitLab.com, etc. to allow project importing.
- Connecting with Google, Twitter, GitHub, etc. to allow OAuth authentication.
We use [the Secure Headers gem][secure_headers] to enable Content
Security Policy headers in the GitLab Rails app.
Some resources on implementing Content Security Policy:
- [MDN Article on CSP][mdn-csp]
- [GitHub’s CSP Journey on the GitHub Engineering Blog][github-eng-csp]
- The Dropbox Engineering Blog's series on CSP: [1][dropbox-csp-1], [2][dropbox-csp-2], [3][dropbox-csp-3], [4][dropbox-csp-4]
### Subresource Integrity (SRI)
Subresource Integrity prevents malicious assets from being provided by a CDN by
guaranteeing that the asset downloaded is identical to the asset the server
is expecting.
The Rails app generates a unique hash of the asset, which is used as the
asset's `integrity` attribute. The browser generates the hash of the asset
on-load and will reject the asset if the hashes do not match.
All CSS and JavaScript assets should use Subresource Integrity. For implementation details,
see the documentation for [the Sprockets implementation of SRI][sprockets-sri].
Some resources on implementing Subresource Integrity:
- [MDN Article on SRI][mdn-sri]
- [Subresource Integrity on the GitHub Engineering Blog][github-eng-sri]
-->
### Including external resources
External fonts, CSS, and JavaScript should never be used with the exception of
Google Analytics and Piwik - and only when the instance has enabled it. Assets
should always be hosted and served locally from the GitLab instance. Embedded
resources via `iframes` should never be used except in certain circumstances
such as with ReCaptcha, which cannot be used without an `iframe`.
### Avoiding inline scripts and styles
In order to protect users from [XSS vulnerabilities][xss], we will disable inline scripts in the future using Content Security Policy.
While inline scripts can be useful, they're also a security concern. If
user-supplied content is unintentionally left un-sanitized, malicious users can
inject scripts into the web app.
Inline styles should be avoided in almost all cases, they should only be used
when no alternatives can be found. This allows reusability of styles as well as
readability.
## Style guides and linting
See the relevant style guides for our guidelines and for information on linting:
- [SCSS][scss-style-guide]
- JavaScript - We defer to [AirBnb][airbnb-js-style-guide] on most style-related
conventions and enforce them with eslint. See [our current .eslintrc][eslintrc]
for specific rules and patterns.
## Testing
Feature tests need to be written for all new features. Regression tests
also need to be written for all bug fixes to prevent them from occurring
again in the future.
See [the Testing Standards and Style Guidelines](testing.md) for more
information.
### Running frontend tests
`rake karma` runs the frontend-only (JavaScript) tests.
It consists of two subtasks:
- `rake karma:fixtures` (re-)generates fixtures
- `rake karma:tests` actually executes the tests
As long as the fixtures don't change, `rake karma:tests` is sufficient
(and saves you some time).
Please note: Not all of the frontend fixtures are generated. Some are still static
files. These will not be touched by `rake karma:fixtures`.
## Design Patterns
### Singletons
When exactly one object is needed for a given task, prefer to define it as a
`class` rather than as an object literal. Prefer also to explicitly restrict
instantiation, unless flexibility is important (e.g. for testing).
```javascript
// bad
gl.MyThing = {
prop1: 'hello',
method1: () => {}
};
// good
class MyThing {
constructor() {
this.prop1 = 'hello';
}
method1() {}
}
gl.MyThing = new MyThing();
// best
let singleton;
class MyThing {
constructor() {
if (!singleton) {
singleton = this;
singleton.init();
}
return singleton;
}
init() {
this.prop1 = 'hello';
}
method1() {}
}
gl.MyThing = MyThing;
```
### Manipulating the DOM in a JS Class
When writing a class that needs to manipulate the DOM guarantee a container option is provided.
This is useful when we need that class to be instantiated more than once in the same page.
Bad:
```javascript
class Foo {
constructor() {
document.querySelector('.bar');
}
}
new Foo();
```
Good:
```javascript
class Foo {
constructor(opts) {
document.querySelector(`${opts.container} .bar`);
}
}
new Foo({ container: '.my-element' });
```
You can find an example of the above in this [class][container-class-example];
## Supported browsers
For our currently-supported browsers, see our [requirements][requirements].
## Gotchas
### Spec errors due to use of ES6 features in `.js` files
If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being
thrown in Karma, Spinach, or Rspec tests but can't reproduce them manually,
you may have included `ES6`-style JavaScript in files that don't have the
`.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file
you're working in (`git mv <file.js> <file.js.es6>`).
### Spec errors due to use of unsupported JavaScript
Similar errors will be thrown if you're using JavaScript features not yet
supported by our test runner's version of webkit, whether or not you've updated
the file extension. Examples of unsupported JavaScript features are:
- Array.from
- Array.find
- Array.first
- Object.assign
- Async functions
- Generators
- Array destructuring
- For Of
- Symbol/Symbol.iterator
- Spread
Until these are polyfilled or transpiled appropriately, they should not be used.
Please update this list with additional unsupported features or when any of
these are made usable.
### Spec errors due to JavaScript not enabled
If, as a result of a change you've made, a feature now depends on JavaScript to
run correctly, you need to make sure a JavaScript web driver is enabled when
specs are run. If you don't you'll see vague error messages from the spec
runner, and an explosion of vague console errors in the HTML snapshot.
To enable a JavaScript driver in an `rspec` test, add `js: true` to the
individual spec or the context block containing multiple specs that need
JavaScript enabled:
```ruby
# For one spec
it 'presents information about abuse report', js: true do
# assertions...
end
describe "Admin::AbuseReports", js: true do
it 'presents information about abuse report' do
# assertions...
end
it 'shows buttons for adding to abuse report' do
# assertions...
end
end
```
In Spinach, the JavaScript driver is enabled differently. In the `*.feature`
file for the failing spec, add the `@javascript` flag above the Scenario:
```
@javascript
Scenario: Developer can approve merge request
Given I am a "Shop" developer
And I visit project "Shop" merge requests page
And merge request 'Bug NS-04' must be approved
And I click link "Bug NS-04"
When I click link "Approve"
Then I should see approved merge request "Bug NS-04"
```
# Frontend Development Guidelines
[rails]: http://rubyonrails.org/
[haml]: http://haml.info/
[hamlit]: https://github.com/k0kubun/hamlit
[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
[scss]: http://sass-lang.com/
[es6]: https://babeljs.io/
[sprockets]: https://github.com/rails/sprockets
[jquery]: https://jquery.com/
[vue]: http://vuejs.org/
[vue-docs]: http://vuejs.org/guide/index.html
[web-page-test]: http://www.webpagetest.org/
[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
[browser-diet]: https://browserdiet.com/
[d3]: https://d3js.org/
[chartjs]: http://www.chartjs.org/
[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
[observatory-cli]: https://github.com/mozilla/http-observatory-cli
[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
[secure_headers]: https://github.com/twitter/secureheaders
[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
[github-eng-sri]: http://githubengineering.com/subresource-integrity/
[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
[scss-style-guide]: scss_styleguide.md
[requirements]: ../install/requirements.md#supported-web-browsers
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
[environments-table]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/environments
[page_specific_javascript]: https://docs.gitlab.com/ce/development/frontend.html#page-specific-javascript
[component-system]: https://vuejs.org/v2/guide/#Composing-with-Components
[state-management]: https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch
[vue-resource-repo]: https://github.com/pagekit/vue-resource
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
[airbnb-js-style-guide]: https://github.com/airbnb/javascript
[eslintrc]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.eslintrc
[team-page]: https://about.gitlab.com/team
[vue-section]: https://docs.gitlab.com/ce/development/frontend.html#how-to-build-a-new-feature-with-vue-js
[container-class-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/mini_pipeline_graph_dropdown.js
This page has moved [here](fe_guide/index.md).
......@@ -42,6 +42,20 @@ To run several tests inside one directory:
If you want to use [Spring](https://github.com/rails/spring) set
`ENABLE_SPRING=1` in your environment.
## Compile Frontend Assets
You shouldn't ever need to compile frontend assets manually in development, but
if you ever need to test how the assets get compiled in a production
environment you can do so with the following command:
```
RAILS_ENV=production NODE_ENV=production bundle exec rake gitlab:assets:compile
```
This will compile and minify all JavaScript and CSS assets and copy them along
with all other frontend assets (images, fonts, etc) into `/public/assets` where
they can be easily inspected.
## Generate API documentation for project services (e.g. Slack)
```
......
......@@ -34,16 +34,17 @@ GitLab uses [factory_girl] as a test fixture replacement.
GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on
the command line via `bundle exec karma`.
- JavaScript tests live in `spec/javascripts/`, matching the folder structure of
`app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js` has a corresponding
`spec/javascripts/behaviors/autosize_spec.js` file.
- JavaScript tests live in `spec/javascripts/`, matching the folder structure
of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js`
has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file.
- Haml fixtures required for JavaScript tests live in
`spec/javascripts/fixtures`. They should contain the bare minimum amount of
markup necessary for the test.
> **Warning:** Keep in mind that a Rails view may change and
invalidate your test, but everything will still pass because your fixture
doesn't reflect the latest view.
doesn't reflect the latest view. Because of this we encourage you to
generate fixtures from actual rails views whenever possible.
- Keep in mind that in a CI environment, these tests are run in a headless
browser and you will not have access to certain APIs, such as
......@@ -53,6 +54,8 @@ the command line via `bundle exec karma`.
[Karma]: https://github.com/karma-runner/karma
[Jasmine]: https://github.com/jasmine/jasmine
For more information, see the [frontend testing guide](fe_guide/testing.md).
## RSpec
### General Guidelines
......
......@@ -109,7 +109,8 @@ Then select 'Internet Site' and press enter to confirm the hostname.
## 2. Ruby
**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future.
**Note:** The current supported Ruby version is 2.3.x. GitLab 9.0 dropped support
for Ruby 2.1.x.
The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example,
......
# Profile Settings
# Profile settings
- [Preferences](preferences.md)
- [Two-factor Authentication (2FA)](two_factor_authentication.md)
- [Preferences](../user/profile/preferences.md)
- [Two-factor Authentication (2FA)](../user/profile/account/two_factor_authentication.md)
# Profile Preferences
Settings in the **Profile > Preferences** page allow the user to customize
various aspects of the site to their liking.
## Syntax highlighting theme
_GitLab uses the [rouge ruby library][rouge] for syntax highlighting. For a
list of supported languages visit the rouge website._
Changing this setting allows the user to customize the theme used when viewing
syntax highlighted code on the site.
The default is **White**.
## Behavior
### Default Dashboard
For users who have access to a large number of projects but only keep up with a
select few, the amount of activity on the default Dashboard page can be
overwhelming.
Changing this setting allows the user to redefine what their default dashboard
will be. Setting it to **Starred Projects** will make that Dashboard view the
default when signing in or clicking the application logo in the upper left.
The default is **Your Projects**.
### Default Project view
It allows user to choose what content he or she want to see on project page.
The default is **Readme**.
[rouge]: http://rouge.jneen.net/ "Rouge website"
This document was moved to [another location](../user/profile/preferences.md).
......@@ -25,9 +25,8 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
### 3. Update Ruby
We will continue supporting Ruby < 2.3 for the time being but we recommend you
upgrade to Ruby 2.3 if you're running a source installation, as this is the same
version that ships with our Omnibus package.
NOTE: GitLab 9.0 only supports Ruby 2.3.x and dropped support for Ruby 2.1.x. Be
sure to upgrade your interpreter if necessary.
You can check which version you are running with `ruby -v`.
......
# Profile preferences
A user's profile preferences page allows the user to customize various aspects
of GitLab to their liking.
To navigate to your profile's preferences, click your avatar icon in the top
right corner and select **Settings**. From there on, choose the **Preferences**
tab.
## Syntax highlighting theme
>**Note:**
GitLab uses the [rouge Ruby library][rouge] for syntax highlighting. For a
list of supported languages visit the rouge website.
Changing this setting allows you to customize the color theme when viewing any
syntax highlighted code on GitLab.
The default one is **White**, and you can choose among 5 different colors:
- White
- Dark
- Solarized light
- Solarized dark
- Monokai
## Behavior
The following settings allow you to customize the behavior of GitLab's layout
and default views of your dashboard and the projects' landing pages.
### Layout width
GitLab can be set up to use different widths depending on your liking. Choose
between the fixed (max. 1200px) and the fluid (100%) application layout.
### Default dashboard
For users who have access to a large number of projects but only keep up with a
select few, the amount of activity on the default Dashboard page can be
overwhelming. Changing this setting allows you to redefine what your default
dashboard will be.
You have 6 options here that you can use for your default dashboard view:
- Your projects (default)
- Starred projects
- Your projects' activity
- Starred projects' activity
- Your groups
- Your [Todos]
### Default project view
The default project view settings allows you to choose what content you want to
see on a project's landing page.
You can choose between 2 options:
- Show the files and the readme (default)
- Show the project's activity
[rouge]: http://rouge.jneen.net/ "Rouge website"
[todos]: ../../workflow/todos.md
module Gitlab
module EtagCaching
class Store
EXPIRY_TIME = 10.minutes
EXPIRY_TIME = 20.minutes
REDIS_NAMESPACE = 'etag:'.freeze
def get(key)
......
# rubocop:disable Style/ClassVars
# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
# Rack middleware that keeps track of the number of active requests and can block new requests.
module Gitlab
module Testing
class RequestBlockerMiddleware
@@num_active_requests = Concurrent::AtomicFixnum.new(0)
@@block_requests = Concurrent::AtomicBoolean.new(false)
# Returns the number of requests the server is currently processing.
def self.num_active_requests
@@num_active_requests.value
end
# Prevents the server from accepting new requests. Any new requests will return an HTTP
# 503 status.
def self.block_requests!
@@block_requests.value = true
end
# Allows the server to accept requests again.
def self.allow_requests!
@@block_requests.value = false
end
def initialize(app)
@app = app
end
def call(env)
increment_active_requests
if block_requests?
block_request(env)
else
@app.call(env)
end
ensure
decrement_active_requests
end
private
def block_requests?
@@block_requests.true?
end
def block_request(env)
[503, {}, []]
end
def increment_active_requests
@@num_active_requests.increment
end
def decrement_active_requests
@@num_active_requests.decrement
end
end
end
end
......@@ -34,6 +34,7 @@ feature 'Admin updates settings', feature: true do
fill_in 'Username', with: 'test_user'
fill_in 'service_push_channel', with: '#test_channel'
page.check('Notify only broken pipelines')
page.check('Notify only default branch')
check_all_events
click_on 'Save'
......
require 'spec_helper'
feature 'Member autocomplete', feature: true do
include WaitForAjax
feature 'Member autocomplete', :js do
let(:project) { create(:project, :public) }
let(:user) { create(:user) }
let(:participant) { create(:user) }
let(:author) { create(:user) }
let(:note) { create(:note, noteable: noteable, project: noteable.project) }
before do
allow_any_instance_of(Commit).to receive(:author).and_return(author)
login_as user
end
shared_examples "open suggestions" do
it 'displays suggestions' do
expect(page).to have_selector('.atwho-view', visible: true)
note # actually create the note
login_as(user)
end
it 'suggests author' do
page.within('.atwho-view', visible: true) do
expect(page).to have_content(author.username)
shared_examples "open suggestions when typing @" do
before do
page.within('.new-note') do
find('#note_note').send_keys('@')
end
end
it 'suggests participant' do
it 'suggests noteable author and note author' do
page.within('.atwho-view', visible: true) do
expect(page).to have_content(participant.username)
expect(page).to have_content(author.username)
expect(page).to have_content(note.author.username)
end
end
end
context 'adding a new note on a Issue', js: true do
context 'adding a new note on a Issue' do
let(:noteable) { create(:issue, author: author, project: project) }
before do
issue = create(:issue, author: author, project: project)
create(:note, note: 'Ultralight Beam', noteable: issue,
project: project, author: participant)
visit_issue(project, issue)
visit namespace_project_issue_path(project.namespace, project, noteable)
end
context 'when typing @' do
include_examples "open suggestions"
before do
open_member_suggestions
end
end
include_examples "open suggestions when typing @"
end
context 'adding a new note on a Merge Request ', js: true do
before do
merge = create(:merge_request, source_project: project, target_project: project, author: author)
create(:note, note: 'Ultralight Beam', noteable: merge,
project: project, author: participant)
visit_merge_request(project, merge)
context 'adding a new note on a Merge Request' do
let(:noteable) do
create(:merge_request, source_project: project,
target_project: project, author: author)
end
context 'when typing @' do
include_examples "open suggestions"
before do
open_member_suggestions
end
visit namespace_project_merge_request_path(project.namespace, project, noteable)
end
end
context 'adding a new note on a Commit ', js: true do
let(:commit) { project.commit }
before do
allow(commit).to receive(:author).and_return(author)
create(:note_on_commit, author: participant, project: project, commit_id: project.repository.commit.id, note: 'No More Parties in LA')
visit_commit(project, commit)
include_examples "open suggestions when typing @"
end
context 'when typing @' do
include_examples "open suggestions"
before do
open_member_suggestions
end
end
end
context 'adding a new note on a Commit' do
let(:noteable) { project.commit }
let(:note) { create(:note_on_commit, project: project, commit_id: project.commit.id) }
def open_member_suggestions
page.within('.new-note') do
find('#note_note').send_keys('@')
end
wait_for_ajax
end
def visit_issue(project, issue)
visit namespace_project_issue_path(project.namespace, project, issue)
end
before do
allow_any_instance_of(Commit).to receive(:author).and_return(author)
def visit_merge_request(project, merge)
visit namespace_project_merge_request_path(project.namespace, project, merge)
visit namespace_project_commit_path(project.namespace, project, noteable)
end
def visit_commit(project, commit)
visit namespace_project_commit_path(project.namespace, project, commit)
include_examples "open suggestions when typing @"
end
end
......@@ -58,7 +58,7 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
});
describe('search', () => {
const defaultParams = '?scope=all&utf8=&state=opened';
const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
it('should search with a single word', (done) => {
input.value = 'searchTerm';
......
import Vue from 'vue';
import VueResource from 'vue-resource';
import Poll from '~/lib/utils/poll';
Vue.use(VueResource);
class ServiceMock {
constructor(endpoint) {
this.service = Vue.resource(endpoint);
}
fetch() {
return this.service.get();
}
}
describe('Poll', () => {
let callbacks;
beforeEach(() => {
callbacks = {
success: () => {},
error: () => {},
};
spyOn(callbacks, 'success');
spyOn(callbacks, 'error');
});
it('calls the success callback when no header for interval is provided', (done) => {
const successInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), { status: 200 }));
};
Vue.http.interceptors.push(successInterceptor);
new Poll({
resource: new ServiceMock('endpoint'),
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
setTimeout(() => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
done();
}, 0);
Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor);
});
it('calls the error callback whe the http request returns an error', (done) => {
const errorInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), { status: 500 }));
};
Vue.http.interceptors.push(errorInterceptor);
new Poll({
resource: new ServiceMock('endpoint'),
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
setTimeout(() => {
expect(callbacks.success).not.toHaveBeenCalled();
expect(callbacks.error).toHaveBeenCalled();
done();
}, 0);
Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor);
});
it('should call the success callback when the interval header is -1', (done) => {
const intervalInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': -1 } }));
};
Vue.http.interceptors.push(intervalInterceptor);
new Poll({
resource: new ServiceMock('endpoint'),
method: 'fetch',
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
setTimeout(() => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
done();
}, 0);
Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor);
});
it('starts polling when http status is 200 and interval header is provided', (done) => {
const pollInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), { status: 200, headers: { 'poll-interval': 2 } }));
};
Vue.http.interceptors.push(pollInterceptor);
const service = new ServiceMock('endpoint');
spyOn(service, 'fetch').and.callThrough();
new Poll({
resource: service,
method: 'fetch',
data: { page: 1 },
successCallback: callbacks.success,
errorCallback: callbacks.error,
}).makeRequest();
setTimeout(() => {
expect(service.fetch.calls.count()).toEqual(2);
expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
done();
}, 5);
Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
});
});
......@@ -3,35 +3,55 @@ import UserCallout from '~/user_callout';
const USER_CALLOUT_COOKIE = 'user_callout_dismissed';
describe('UserCallout', () => {
describe('UserCallout', function () {
const fixtureName = 'static/user_callout.html.raw';
preloadFixtures(fixtureName);
beforeEach(function () {
beforeEach(() => {
loadFixtures(fixtureName);
Cookies.remove(USER_CALLOUT_COOKIE);
this.userCallout = new UserCallout();
this.closeButton = $('.close-user-callout');
this.userCalloutBtn = $('.user-callout-btn');
this.userCalloutContainer = $('.user-callout');
Cookies.set(USER_CALLOUT_COOKIE, 'false');
});
afterEach(function () {
Cookies.set(USER_CALLOUT_COOKIE, 'false');
it('does not show when cookie is set not defined', () => {
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeUndefined();
expect(this.userCalloutContainer.is(':visible')).toBe(true);
});
it('shows when cookie is set to false', function () {
it('shows when cookie is set to false', () => {
Cookies.set(USER_CALLOUT_COOKIE, 'false');
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBeDefined();
expect(this.userCalloutContainer.is(':visible')).toBe(true);
});
it('hides when user clicks on the dismiss-icon', function () {
it('hides when user clicks on the dismiss-icon', () => {
this.closeButton.click();
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
});
it('hides when user clicks on the "check it out" button', function () {
it('hides when user clicks on the "check it out" button', () => {
this.userCalloutBtn.click();
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
});
});
describe('UserCallout when cookie is present', function () {
const fixtureName = 'static/user_callout.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
loadFixtures(fixtureName);
Cookies.set(USER_CALLOUT_COOKIE, 'true');
this.userCallout = new UserCallout();
this.userCalloutContainer = $('.user-callout');
});
it('removes the DOM element', () => {
expect(this.userCalloutContainer.length).toBe(0);
});
});
......@@ -33,6 +33,7 @@ describe Boards::Issues::ListService, services: true do
let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
let!(:closed_issue3) { create(:issue, :closed, project: project) }
let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development]) }
before do
project.team << [user, :developer]
......@@ -74,7 +75,7 @@ describe Boards::Issues::ListService, services: true do
issues = described_class.new(project, user, params).execute
expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]
expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1]
end
it 'returns opened issues that have label list applied when listing issues from a label list' do
......
......@@ -138,7 +138,7 @@ describe Issuable::BulkUpdateService, services: true do
let(:labels) { [bug, regression] }
it 'updates the labels of all issues passed to the labels passed' do
expect(issues.map(&:reload).map(&:label_ids)).to all(eq(labels.map(&:id)))
expect(issues.map(&:reload).map(&:label_ids)).to all(match_array(labels.map(&:id)))
end
it 'does not update issues not passed in' do
......
......@@ -35,7 +35,8 @@ RSpec.configure do |config|
config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
config.include WaitForAjax, type: :feature
config.include WaitForRequests, :js
config.include WaitForAjax, :js
config.include StubConfiguration
config.include EmailHelpers, type: :mailer
config.include TestEnv
......
......@@ -324,5 +324,24 @@ RSpec.shared_examples 'slack or mattermost notifications' do
it_behaves_like 'call Slack/Mattermost API'
end
end
context 'only notify for the default branch' do
context 'when enabled' do
let(:pipeline) do
create(:ci_pipeline, project: project, status: 'failed', ref: 'not-the-default-branch')
end
before do
chat_service.notify_only_default_branch = true
end
it 'does not call the Slack/Mattermost API for pipeline events' do
data = Gitlab::DataBuilder::Pipeline.build(pipeline)
result = chat_service.execute(data)
expect(result).to be_falsy
end
end
end
end
end
module WaitForRequests
extend self
# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
def wait_for_requests_complete
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
wait_for('pending AJAX requests complete') do
Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero?
end
ensure
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
end
# Waits until the passed block returns true
def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01)
wait_until = Time.now + max_wait_time.seconds
loop do
break if yield
if Time.now > wait_until
raise "Condition not met: #{condition_name}"
else
sleep(polling_interval)
end
end
end
end
RSpec.configure do |config|
config.after(:each, :js) do
wait_for_requests_complete
end
end
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