Commit b35b9ac7 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 81f7adf0
......@@ -10,6 +10,7 @@ plugins:
- import
- "@gitlab/i18n"
- "@gitlab/vue-i18n"
- no-jquery
settings:
import/resolver:
webpack:
......@@ -36,6 +37,11 @@ rules:
vue/no-use-v-if-with-v-for: off
vue/no-v-html: off
vue/use-v-on-exact: off
no-jquery/no-ajax: error
no-jquery/no-ajax-events: error
no-jquery/no-load: error
no-jquery/no-load-shorthand: error
no-jquery/no-serialize: error
overrides:
files:
- '**/spec/**/*'
......
......@@ -25,7 +25,7 @@ Then leave running while monitoring and performing some testing through web, api
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlab.net/gitlab/devgitlaborg/?query=is%3Aunresolved)
## 2. Staging Trial
......@@ -41,7 +41,7 @@ Then leave running while monitoring for at least **15 minutes** while performing
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved)
## 4. Production Server Version Check
......@@ -57,7 +57,7 @@ Then leave running while monitoring for at least **15 minutes** while performing
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved)
## 6. Low Impact Check
......@@ -69,7 +69,7 @@ Then leave running while monitoring for at least **30 minutes** while performing
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved)
## 7. Mid Impact Trial
......@@ -81,7 +81,7 @@ Then leave running while monitoring for at least **12 hours** while performing s
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlab.net/gitlab/gitlabcom/?query=is%3Aunresolved)
## 8. Full Impact Trial
......@@ -93,7 +93,7 @@ Then leave running while monitoring for at least **1 week**.
- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved)
- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlab.net/gitlab/devgitlaborg/?query=is%3Aunresolved)
#### Success?
......
......@@ -446,7 +446,7 @@ group :ed25519 do
end
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 1.58.0'
gem 'gitaly', '~> 1.65.0'
gem 'grpc', '~> 1.19.0'
......
......@@ -358,7 +358,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
git (1.5.0)
gitaly (1.58.0)
gitaly (1.65.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-labkit (0.5.2)
......@@ -1168,7 +1168,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 1.58.0)
gitaly (~> 1.65.0)
github-markup (~> 1.7.0)
gitlab-labkit (~> 0.5)
gitlab-license (~> 1.0)
......
export const initSidebarTracking = () => {};
export const trackEvent = () => {};
// Noop function which has a EE counter-part
export default () => {};
......@@ -22,6 +22,7 @@ export default class FilterableList {
getPagePath() {
const action = this.filterForm.getAttribute('action');
// eslint-disable-next-line no-jquery/no-serialize
const params = $(this.filterForm).serialize();
return `${action}${action.indexOf('?') > 0 ? '&' : '?'}${params}`;
}
......
......@@ -42,6 +42,7 @@ export default class IntegrationSettingsForm {
// and test the service using provided configuration.
if (this.$form.get(0).checkValidity() && this.canTestService) {
e.preventDefault();
// eslint-disable-next-line no-jquery/no-serialize
this.testSettings(this.$form.serialize());
}
}
......
import Vue from 'vue';
import { initSidebarTracking } from 'ee_else_ce/event_tracking/issue_sidebar';
import issuableApp from './components/app.vue';
import { parseIssuableData } from './utils/parse_data';
......@@ -9,9 +8,6 @@ export default function initIssueableApp() {
components: {
issuableApp,
},
mounted() {
initSidebarTracking();
},
render(createElement) {
return createElement('issuable-app', {
props: parseIssuableData(),
......
......@@ -314,6 +314,7 @@ document.addEventListener('DOMContentLoaded', () => {
const action = `${this.action}${link.search === '' ? '?' : '&'}`;
event.preventDefault();
// eslint-disable-next-line no-jquery/no-serialize
visitUrl(`${action}${$(this).serialize()}`);
});
......
......@@ -1461,6 +1461,7 @@ export default class Notes {
getFormData($form) {
const content = $form.find('.js-note-text').val();
return {
// eslint-disable-next-line no-jquery/no-serialize
formData: $form.serialize(),
formContent: _.escape(content),
formAction: $form.attr('action'),
......
......@@ -19,7 +19,9 @@ export default {
<gl-button
ref="button"
v-gl-tooltip
class="note-action-button js-note-action-reply"
class="note-action-button"
data-track-event="click_button"
data-track-label="reply_comment_button"
variant="transparent"
:title="__('Reply to comment')"
@click="$emit('startReplying')"
......
import Vue from 'vue';
import initNoteStats from 'ee_else_ce/event_tracking/notes';
import notesApp from './components/notes_app.vue';
import initDiscussionFilters from './discussion_filters';
import createStore from './stores';
......@@ -39,9 +38,6 @@ document.addEventListener('DOMContentLoaded', () => {
notesData: JSON.parse(notesDataset.notesData),
};
},
mounted() {
initNoteStats();
},
render(createElement) {
return createElement('notes-app', {
props: {
......
......@@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', () => {
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
// eslint-disable-next-line no-jquery/no-load
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
initPipelines();
});
......@@ -21,6 +21,7 @@ document.addEventListener('DOMContentLoaded', () => {
}).bindEvents();
initNotes();
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
// eslint-disable-next-line no-jquery/no-load
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
fetchCommitMergeRequests();
initDiffNotes();
......
<script>
import { n__ } from '~/locale';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
name: 'AssigneeTitle',
......@@ -30,11 +29,6 @@ export default {
return n__('Assignee', `%d Assignees`, assignees);
},
},
methods: {
trackEdit() {
trackEvent('click_edit_button', 'assignee');
},
},
};
</script>
<template>
......@@ -45,7 +39,9 @@ export default {
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right"
href="#"
@click.prevent="trackEdit"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="assignee"
>
{{ __('Edit') }}
</a>
......
......@@ -5,7 +5,6 @@ import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
components: {
......@@ -52,11 +51,6 @@ export default {
toggleForm() {
this.edit = !this.edit;
},
onEditClick() {
this.toggleForm();
trackEvent('click_edit_button', 'confidentiality');
},
updateConfidentialAttribute(confidential) {
this.service
.update('issue', { confidential })
......@@ -88,7 +82,10 @@ export default {
v-if="isEditable"
class="float-right confidential-edit"
href="#"
@click.prevent="onEditClick"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="confidentiality"
@click.prevent="toggleForm"
>
{{ __('Edit') }}
</a>
......
......@@ -6,7 +6,6 @@ import issuableMixin from '~/vue_shared/mixins/issuable';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '~/sidebar/event_hub';
import editForm from './edit_form.vue';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
export default {
components: {
......@@ -66,11 +65,6 @@ export default {
toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
},
onEditClick() {
this.toggleForm();
trackEvent('click_edit_button', 'lock_issue');
},
updateLockedAttribute(locked) {
this.mediator.service
.update(this.issuableType, {
......@@ -114,7 +108,10 @@ export default {
v-if="isEditable"
class="float-right lock-edit"
type="button"
@click.prevent="onEditClick"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
data-track-property="lock_issue"
@click.prevent="toggleForm"
>
{{ __('Edit') }}
</button>
......
<script>
import { __ } from '~/locale';
import Tracking from '~/tracking';
import icon from '~/vue_shared/components/icon.vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import eventHub from '../../event_hub';
import { trackEvent } from 'ee_else_ce/event_tracking/issue_sidebar';
const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off';
......@@ -19,6 +19,7 @@ export default {
icon,
toggleButton,
},
mixins: [Tracking.mixin({ label: 'right_sidebar' })],
props: {
loading: {
type: Boolean,
......@@ -65,7 +66,10 @@ export default {
// Component event emission.
this.$emit('toggleSubscription', this.id);
trackEvent('toggle_button', 'notifications', this.subscribed ? 0 : 1);
this.track('toggle_button', {
property: 'notifications',
value: this.subscribed ? 0 : 1,
});
},
onClickCollapsedIcon() {
this.$emit('toggleSidebar');
......
import $ from 'jquery';
import _ from 'underscore';
const DEFAULT_SNOWPLOW_OPTIONS = {
namespace: 'gl',
......@@ -14,18 +14,31 @@ const DEFAULT_SNOWPLOW_OPTIONS = {
linkClickTracking: false,
};
const extractData = (el, opts = {}) => {
const { trackEvent, trackLabel = '', trackProperty = '' } = el.dataset;
let trackValue = el.dataset.trackValue || el.value || '';
if (el.type === 'checkbox' && !el.checked) trackValue = false;
return [
trackEvent + (opts.suffix || ''),
{
label: trackLabel,
property: trackProperty,
value: trackValue,
},
];
const eventHandler = (e, func, opts = {}) => {
const el = e.target.closest('[data-track-event]');
const action = el && el.dataset.trackEvent;
if (!action) return;
let value = el.dataset.trackValue || el.value || undefined;
if (el.type === 'checkbox' && !el.checked) value = false;
const data = {
label: el.dataset.trackLabel,
property: el.dataset.trackProperty,
value,
context: el.dataset.trackContext,
};
func(opts.category, action + (opts.suffix || ''), _.omit(data, _.isUndefined));
};
const eventHandlers = (category, func) => {
const handler = opts => e => eventHandler(e, func, { ...{ category }, ...opts });
const handlers = [];
handlers.push({ name: 'click', func: handler() });
handlers.push({ name: 'show.bs.dropdown', func: handler({ suffix: '_show' }) });
handlers.push({ name: 'hide.bs.dropdown', func: handler({ suffix: '_hide' }) });
return handlers;
};
export default class Tracking {
......@@ -39,49 +52,43 @@ export default class Tracking {
return typeof window.snowplow === 'function' && this.trackable();
}
static event(category = document.body.dataset.page, event = 'generic', data = {}) {
static event(category = document.body.dataset.page, action = 'generic', data = {}) {
if (!this.enabled()) return false;
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
if (!category) throw new Error('Tracking: no category provided for tracking.');
return window.snowplow(
'trackStructEvent',
category,
event,
Object.assign({}, { label: '', property: '', value: '' }, data),
);
const { label, property, value, context } = data;
const contexts = context ? [context] : undefined;
return window.snowplow('trackStructEvent', category, action, label, property, value, contexts);
}
constructor(category = document.body.dataset.page) {
this.category = category;
}
static bindDocument(category = document.body.dataset.page, documentOverride = null) {
const el = documentOverride || document;
if (!this.enabled() || el.trackingBound) return [];
bind(container = document) {
if (!this.constructor.enabled()) return;
container.querySelectorAll(`[data-track-event]`).forEach(el => {
if (this.customHandlingFor(el)) return;
// jquery is required for select2, so we use it always
// see: https://github.com/select2/select2/issues/4686
$(el).on('click', this.eventHandler(this.category));
});
}
el.trackingBound = true;
customHandlingFor(el) {
const classes = el.classList;
// bootstrap dropdowns
if (classes.contains('dropdown')) {
$(el).on('show.bs.dropdown', this.eventHandler(this.category, { suffix: '_show' }));
$(el).on('hide.bs.dropdown', this.eventHandler(this.category, { suffix: '_hide' }));
return true;
}
return false;
const handlers = eventHandlers(category, (...args) => this.event(...args));
handlers.forEach(event => el.addEventListener(event.name, event.func));
return handlers;
}
eventHandler(category = null, opts = {}) {
return e => {
this.constructor.event(category || this.category, ...extractData(e.currentTarget, opts));
static mixin(opts) {
return {
data() {
return {
tracking: {
// eslint-disable-next-line no-underscore-dangle
category: this.$options.name || this.$options._componentTag,
},
};
},
methods: {
track(action, data) {
const category = opts.category || data.category || this.tracking.category;
Tracking.event(category || 'unspecified', action, { ...opts, ...this.tracking, ...data });
},
},
};
}
}
......@@ -89,7 +96,7 @@ export default class Tracking {
export function initUserTracking() {
if (!Tracking.enabled()) return;
const opts = Object.assign({}, DEFAULT_SNOWPLOW_OPTIONS, window.snowplowOptions);
const opts = { ...DEFAULT_SNOWPLOW_OPTIONS, ...window.snowplowOptions };
window.snowplow('newTracker', opts.namespace, opts.hostname, opts);
window.snowplow('enableActivityTracking', 30, 30);
......@@ -97,4 +104,6 @@ export function initUserTracking() {
if (opts.formTracking) window.snowplow('enableFormTracking');
if (opts.linkClickTracking) window.snowplow('enableLinkClickTracking');
Tracking.bindDocument();
}
......@@ -12,7 +12,7 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
position: sticky;
position: -webkit-sticky;
top: $flash-container-top;
z-index: 200;
z-index: 251;
.flash-content {
box-shadow: 0 2px 4px 0 $notification-box-shadow-color;
......
......@@ -133,18 +133,28 @@ class Repository
end
end
def commits(ref = nil, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil, all: nil)
# the opts are:
# - :path
# - :limit
# - :offset
# - :skip_merges
# - :after
# - :before
# - :all
# - :first_parent
def commits(ref = nil, opts = {})
options = {
repo: raw_repository,
ref: ref,
path: path,
limit: limit,
offset: offset,
after: after,
before: before,
follow: Array(path).length == 1,
skip_merges: skip_merges,
all: all
path: opts[:path],
follow: Array(opts[:path]).length == 1,
limit: opts[:limit],
offset: opts[:offset],
skip_merges: !!opts[:skip_merges],
after: opts[:after],
before: opts[:before],
all: !!opts[:all],
first_parent: !!opts[:first_parent]
}
commits = Gitlab::Git::Commit.where(options)
......
---
title: Add first_parent option to list commits api
merge_request: 32410
author: jhenkens
type: added
......@@ -13,6 +13,7 @@ Bundler.require(*Rails.groups)
module Gitlab
class Application < Rails::Application
require_dependency Rails.root.join('lib/gitlab')
require_dependency Rails.root.join('lib/gitlab/utils')
require_dependency Rails.root.join('lib/gitlab/redis/wrapper')
require_dependency Rails.root.join('lib/gitlab/redis/cache')
require_dependency Rails.root.join('lib/gitlab/redis/queues')
......@@ -46,9 +47,10 @@ module Gitlab
config.generators.templates.push("#{config.root}/generator_templates")
if Gitlab.ee?
ee_paths = config.eager_load_paths.each_with_object([]) do |path, memo|
ee_path = config.root.join('ee', Pathname.new(path).relative_path_from(config.root))
memo << ee_path.to_s if ee_path.exist?
memo << ee_path.to_s
end
# Eager load should load CE first
......@@ -58,6 +60,7 @@ module Gitlab
# Other than Ruby modules we load EE first
config.paths['lib/tasks'].unshift "#{config.root}/ee/lib/tasks"
config.paths['app/views'].unshift "#{config.root}/ee/app/views"
end
# Rake tasks ignore the eager loading settings, so we need to set the
# autoload paths explicitly
......@@ -178,16 +181,18 @@ module Gitlab
config.assets.paths << "#{config.root}/node_modules/xterm/src/"
config.assets.precompile << "xterm.css"
if Gitlab.ee?
%w[images javascripts stylesheets].each do |path|
config.assets.paths << "#{config.root}/ee/app/assets/#{path}"
config.assets.precompile << "jira_connect.js"
config.assets.precompile << "pages/jira_connect.css"
end
end
# Import path for EE specific SCSS entry point
# In CE it will import a noop file, in EE a functioning file
# Order is important, so that the ee file takes precedence:
config.assets.paths << "#{config.root}/ee/app/assets/stylesheets/_ee"
config.assets.paths << "#{config.root}/ee/app/assets/stylesheets/_ee" if Gitlab.ee?
config.assets.paths << "#{config.root}/app/assets/stylesheets/_ee"
config.assets.paths << "#{config.root}/vendor/assets/javascripts/"
......@@ -197,6 +202,7 @@ module Gitlab
# See https://gitlab.com/gitlab-org/gitlab-foss/issues/64091#note_194512508
config.assets.paths << "#{config.root}/node_modules"
if Gitlab.ee?
# Compile non-JS/CSS assets in the ee/app/assets folder by default
# Mimic sprockets-rails default: https://github.com/rails/sprockets-rails/blob/v3.2.1/lib/sprockets/railtie.rb#L84-L87
LOOSE_EE_APP_ASSETS = lambda do |logical_path, filename|
......@@ -204,6 +210,7 @@ module Gitlab
!['.js', '.css', ''].include?(File.extname(logical_path))
end
config.assets.precompile << LOOSE_EE_APP_ASSETS
end
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
......
......@@ -11,12 +11,13 @@ GET /projects/:id/repository/commits
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
| `ref_name` | string | no | The name of a repository branch, tag or revision range, or if not given the default branch |
| `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
| `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
| `path` | string | no | The file path |
| `all` | boolean | no | Retrieve every commit from the repository |
| `with_stats` | boolean | no | Stats about each commit will be added to the response |
| `first_parent` | boolean | no | Follow only the first parent commit upon seeing a merge commit |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/commits"
......
......@@ -37,6 +37,7 @@ module API
optional :path, type: String, desc: 'The file path'
optional :all, type: Boolean, desc: 'Every commit will be returned'
optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response'
optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges'
use :pagination
end
get ':id/repository/commits' do
......@@ -47,6 +48,7 @@ module API
offset = (params[:page] - 1) * params[:per_page]
all = params[:all]
with_stats = params[:with_stats]
first_parent = params[:first_parent]
commits = user_project.repository.commits(ref,
path: path,
......@@ -54,11 +56,12 @@ module API
offset: offset,
before: before,
after: after,
all: all)
all: all,
first_parent: first_parent)
commit_count =
if all || path || before || after
user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all)
if all || path || before || after || first_parent
user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all, first_parent: first_parent)
else
# Cacheable commit count.
user_project.repository.commit_count_for_ref(ref)
......
......@@ -140,7 +140,8 @@ module Gitlab
request = Gitaly::CountCommitsRequest.new(
repository: @gitaly_repo,
revision: encode_binary(ref),
all: !!options[:all]
all: !!options[:all],
first_parent: !!options[:first_parent]
)
request.after = Google::Protobuf::Timestamp.new(seconds: options[:after].to_i) if options[:after].present?
request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present?
......@@ -325,6 +326,7 @@ module Gitlab
follow: options[:follow],
skip_merges: options[:skip_merges],
all: !!options[:all],
first_parent: !!options[:first_parent],
disable_walk: true # This option is deprecated. The 'walk' implementation is being removed.
)
request.after = GitalyClient.timestamp(options[:after]) if options[:after]
......
import $ from 'jquery';
import { setHTMLFixture } from './helpers/fixtures';
import Tracking, { initUserTracking } from '~/tracking';
describe('Tracking', () => {
let snowplowSpy;
let bindDocumentSpy;
beforeEach(() => {
window.snowplow = window.snowplow || (() => {});
......@@ -17,6 +16,10 @@ describe('Tracking', () => {
});
describe('initUserTracking', () => {
beforeEach(() => {
bindDocumentSpy = jest.spyOn(Tracking, 'bindDocument').mockImplementation(() => null);
});
it('calls through to get a new tracker with the expected options', () => {
initUserTracking();
expect(snowplowSpy).toHaveBeenCalledWith('newTracker', '_namespace_', 'app.gitfoo.com', {
......@@ -50,6 +53,11 @@ describe('Tracking', () => {
expect(snowplowSpy).toHaveBeenCalledWith('enableFormTracking');
expect(snowplowSpy).toHaveBeenCalledWith('enableLinkClickTracking');
});
it('binds the document event handling', () => {
initUserTracking();
expect(bindDocumentSpy).toHaveBeenCalled();
});
});
describe('.event', () => {
......@@ -62,11 +70,15 @@ describe('Tracking', () => {
it('tracks to snowplow (our current tracking system)', () => {
Tracking.event('_category_', '_eventName_', { label: '_label_' });
expect(snowplowSpy).toHaveBeenCalledWith('trackStructEvent', '_category_', '_eventName_', {
label: '_label_',
property: '',
value: '',
});
expect(snowplowSpy).toHaveBeenCalledWith(
'trackStructEvent',
'_category_',
'_eventName_',
'_label_',
undefined,
undefined,
undefined,
);
});
it('skips tracking if snowplow is unavailable', () => {
......@@ -99,83 +111,70 @@ describe('Tracking', () => {
});
describe('tracking interface events', () => {
let eventSpy = null;
let subject = null;
let eventSpy;
const trigger = (selector, eventName = 'click') => {
const event = new Event(eventName, { bubbles: true });
document.querySelector(selector).dispatchEvent(event);
};
beforeEach(() => {
eventSpy = jest.spyOn(Tracking, 'event');
subject = new Tracking('_category_');
Tracking.bindDocument('_category_'); // only happens once
setHTMLFixture(`
<input data-track-event="click_input1" data-track-label="_label_" value="_value_"/>
<input data-track-event="click_input2" data-track-value="_value_override_" value="_value_"/>
<input type="checkbox" data-track-event="toggle_checkbox" value="_value_" checked/>
<input class="dropdown" data-track-event="toggle_dropdown"/>
<div class="js-projects-list-holder"></div>
<div data-track-event="nested_event"><span class="nested"></span></div>
`);
});
it('binds to clicks on elements matching [data-track-event]', () => {
subject.bind(document);
$('[data-track-event="click_input1"]').click();
trigger('[data-track-event="click_input1"]');
expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input1', {
label: '_label_',
value: '_value_',
property: '',
});
});
it('allows value override with the data-track-value attribute', () => {
subject.bind(document);
$('[data-track-event="click_input2"]').click();
trigger('[data-track-event="click_input2"]');
expect(eventSpy).toHaveBeenCalledWith('_category_', 'click_input2', {
label: '',
value: '_value_override_',
property: '',
});
});
it('handles checkbox values correctly', () => {
subject.bind(document);
const $checkbox = $('[data-track-event="toggle_checkbox"]');
$checkbox.click(); // unchecking
trigger('[data-track-event="toggle_checkbox"]'); // checking
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
label: '',
property: '',
value: false,
});
$checkbox.click(); // checking
trigger('[data-track-event="toggle_checkbox"]'); // unchecking
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_checkbox', {
label: '',
property: '',
value: '_value_',
});
});
it('handles bootstrap dropdowns', () => {
new Tracking('_category_').bind(document);
const $dropdown = $('[data-track-event="toggle_dropdown"]');
trigger('[data-track-event="toggle_dropdown"]', 'show.bs.dropdown'); // showing
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', {});
$dropdown.trigger('show.bs.dropdown'); // showing
trigger('[data-track-event="toggle_dropdown"]', 'hide.bs.dropdown'); // hiding
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_show', {
label: '',
property: '',
value: '',
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_hide', {});
});
$dropdown.trigger('hide.bs.dropdown'); // hiding
it('handles nested elements inside an element with tracking', () => {
trigger('span.nested', 'click');
expect(eventSpy).toHaveBeenCalledWith('_category_', 'toggle_dropdown_hide', {
label: '',
property: '',
value: '',
});
expect(eventSpy).toHaveBeenCalledWith('_category_', 'nested_event', {});
});
});
});
import Tracking from '~/tracking';
export default Tracking;
let document;
let handlers;
export function mockTracking(category = '_category_', documentOverride, spyMethod) {
document = documentOverride || window.document;
window.snowplow = () => {};
Tracking.bindDocument(category, document);
return spyMethod ? spyMethod(Tracking, 'event') : null;
}
export function unmockTracking() {
window.snowplow = undefined;
handlers.forEach(event => document.removeEventListener(event.name, event.func));
}
export function triggerEvent(selectorOrEl, eventName = 'click') {
const event = new Event(eventName, { bubbles: true });
const el = typeof selectorOrEl === 'string' ? document.querySelector(selectorOrEl) : selectorOrEl;
el.dispatchEvent(event);
}
......@@ -126,6 +126,7 @@ describe('IntegrationSettingsForm', () => {
spyOn(axios, 'put').and.callThrough();
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
// eslint-disable-next-line no-jquery/no-serialize
formData = integrationSettingsForm.$form.serialize();
});
......
import Vue from 'vue';
import AssigneeTitle from '~/sidebar/components/assignees/assignee_title.vue';
import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
describe('AssigneeTitle component', () => {
let component;
let AssigneeTitleComponent;
let statsSpy;
beforeEach(() => {
statsSpy = spyOnDependency(AssigneeTitle, 'trackEvent');
AssigneeTitleComponent = Vue.extend(AssigneeTitle);
});
......@@ -105,15 +104,20 @@ describe('AssigneeTitle component', () => {
expect(component.$el.querySelector('.edit-link')).not.toBeNull();
});
it('calls trackEvent when edit is clicked', () => {
it('tracks the event when edit is clicked', () => {
component = new AssigneeTitleComponent({
propsData: {
numberOfAssignees: 0,
editable: true,
},
}).$mount();
component.$el.querySelector('.js-sidebar-dropdown-toggle').click();
expect(statsSpy).toHaveBeenCalled();
const spy = mockTracking('_category_', component.$el, spyOn);
triggerEvent('.js-sidebar-dropdown-toggle');
expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
label: 'right_sidebar',
property: 'assignee',
});
});
});
import Vue from 'vue';
import confidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
describe('Confidential Issue Sidebar Block', () => {
let vm1;
let vm2;
let statsSpy;
beforeEach(() => {
statsSpy = spyOnDependency(confidentialIssueSidebar, 'trackEvent');
const Component = Vue.extend(confidentialIssueSidebar);
const service = {
update: () => Promise.resolve(true),
......@@ -70,9 +69,13 @@ describe('Confidential Issue Sidebar Block', () => {
});
});
it('calls trackEvent when "Edit" is clicked', () => {
vm1.$el.querySelector('.confidential-edit').click();
it('tracks the event when "Edit" is clicked', () => {
const spy = mockTracking('_category_', vm1.$el, spyOn);
triggerEvent('.confidential-edit');
expect(statsSpy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
label: 'right_sidebar',
property: 'confidentiality',
});
});
});
import Vue from 'vue';
import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper';
describe('LockIssueSidebar', () => {
let vm1;
let vm2;
let statsSpy;
beforeEach(() => {
statsSpy = spyOnDependency(lockIssueSidebar, 'trackEvent');
const Component = Vue.extend(lockIssueSidebar);
const mediator = {
......@@ -61,10 +60,14 @@ describe('LockIssueSidebar', () => {
});
});
it('calls trackEvent when "Edit" is clicked', () => {
vm1.$el.querySelector('.lock-edit').click();
it('tracks an event when "Edit" is clicked', () => {
const spy = mockTracking('_category_', vm1.$el, spyOn);
triggerEvent('.lock-edit');
expect(statsSpy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
label: 'right_sidebar',
property: 'lock_issue',
});
});
it('displays the edit form when opened from collapsed state', done => {
......
......@@ -2,14 +2,13 @@ import Vue from 'vue';
import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import eventHub from '~/sidebar/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockTracking } from 'spec/helpers/tracking_helper';
describe('Subscriptions', function() {
let vm;
let Subscriptions;
let statsSpy;
beforeEach(() => {
statsSpy = spyOnDependency(subscriptions, 'trackEvent');
Subscriptions = Vue.extend(subscriptions);
});
......@@ -53,6 +52,7 @@ describe('Subscriptions', function() {
vm = mountComponent(Subscriptions, { subscribed: true });
spyOn(eventHub, '$emit');
spyOn(vm, '$emit');
spyOn(vm, 'track');
vm.toggleSubscription();
......@@ -60,11 +60,12 @@ describe('Subscriptions', function() {
expect(vm.$emit).toHaveBeenCalledWith('toggleSubscription', jasmine.any(Object));
});
it('calls trackEvent when toggled', () => {
it('tracks the event when toggled', () => {
vm = mountComponent(Subscriptions, { subscribed: true });
const spy = mockTracking('_category_', vm.$el, spyOn);
vm.toggleSubscription();
expect(statsSpy).toHaveBeenCalled();
expect(spy).toHaveBeenCalled();
});
it('onClickCollapsedIcon method emits `toggleSidebar` event on component', () => {
......
......@@ -279,7 +279,7 @@ describe Repository do
describe '#commits' do
context 'when neither the all flag nor a ref are specified' do
it 'returns every commit from default branch' do
expect(repository.commits(limit: 60).size).to eq(37)
expect(repository.commits(nil, limit: 60).size).to eq(37)
end
end
......@@ -320,7 +320,7 @@ describe Repository do
context "when 'all' flag is set" do
it 'returns every commit from the repository' do
expect(repository.commits(all: true, limit: 60).size).to eq(60)
expect(repository.commits(nil, all: true, limit: 60).size).to eq(60)
end
end
end
......
......@@ -169,6 +169,18 @@ describe API::Commits do
end
end
context 'first_parent optional parameter' do
it 'returns all first_parent commits' do
commit_count = project.repository.count_commits(ref: SeedRepo::Commit::ID, first_parent: true)
get api("/projects/#{project_id}/repository/commits", user), params: { ref_name: SeedRepo::Commit::ID, first_parent: 'true' }
expect(response).to include_pagination_headers
expect(commit_count).to eq(12)
expect(response.headers['X-Total']).to eq(commit_count.to_s)
end
end
context 'with_stats optional parameter' do
let(:project) { create(:project, :public, :repository) }
......
......@@ -4624,6 +4624,11 @@ eslint-plugin-jest@^22.3.0:
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-22.3.0.tgz#a10f10dedfc92def774ec9bb5bfbd2fb8e1c96d2"
integrity sha512-P1mYVRNlOEoO5T9yTqOfucjOYf1ktmJ26NjwjH8sxpCFQa6IhBGr5TpKl3hcAAT29hOsRJVuMWmTsHoUVo9FoA==
eslint-plugin-no-jquery@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-no-jquery/-/eslint-plugin-no-jquery-2.1.0.tgz#d03b74224c5cfbc7fc0bdd12ce4eb400d09e0c0b"
integrity sha512-5sr5tOJRfuRviyAvFTe/mr80TXWxTteD/JHRuJtDN8q/bxAh16eSKoKLAevLC7wZCRN2iwnEfhQPQV4rp/gYtg==
eslint-plugin-promise@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.1.1.tgz#1e08cb68b5b2cd8839f8d5864c796f56d82746db"
......
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