Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb13
-rw-r--r--spec/controllers/projects_controller_spec.rb8
-rw-r--r--spec/factories/ci/builds.rb7
-rw-r--r--spec/factories/commit_statuses.rb4
-rw-r--r--spec/features/atom/users_spec.rb2
-rw-r--r--spec/features/copy_as_gfm_spec.rb6
-rw-r--r--spec/features/environment_spec.rb32
-rw-r--r--spec/features/environments_spec.rb25
-rw-r--r--spec/features/groups_spec.rb40
-rw-r--r--spec/features/issues/award_emoji_spec.rb10
-rw-r--r--spec/features/issues/form_spec.rb14
-rw-r--r--spec/features/markdown_spec.rb8
-rw-r--r--spec/features/merge_requests/form_spec.rb10
-rw-r--r--spec/features/projects_spec.rb2
-rw-r--r--spec/fixtures/markdown.md.erb5
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb2
-rw-r--r--spec/javascripts/awards_handler_spec.js153
-rw-r--r--spec/javascripts/behaviors/bind_in_out_spec.js189
-rw-r--r--spec/javascripts/build_spec.js10
-rw-r--r--spec/javascripts/fixtures/emoji_menu.js4
-rw-r--r--spec/javascripts/gl_emoji_spec.js367
-rw-r--r--spec/lib/banzai/filter/emoji_filter_spec.rb112
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb10
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb54
-rw-r--r--spec/lib/gitlab/award_emoji_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb14
-rw-r--r--spec/lib/gitlab/ci/config/entry/factory_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb79
-rw-r--r--spec/lib/gitlab/ci/config/entry/jobs_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/key_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/status/build/play_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/status/canceled_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/created_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/failed_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/manual_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/status/pending_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/running_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/skipped_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/success_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb54
-rw-r--r--spec/models/appearance_spec.rb2
-rw-r--r--spec/models/chat_team_spec.rb10
-rw-r--r--spec/models/ci/build_spec.rb74
-rw-r--r--spec/models/ci/pipeline_spec.rb16
-rw-r--r--spec/models/commit_status_spec.rb27
-rw-r--r--spec/models/concerns/has_status_spec.rb36
-rw-r--r--spec/models/group_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb14
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb6
-rw-r--r--spec/models/project_spec.rb1
-rw-r--r--spec/models/upload_spec.rb151
-rw-r--r--spec/models/user_spec.rb1
-rw-r--r--spec/requests/api/environments_spec.rb5
-rw-r--r--spec/routing/project_routing_spec.rb3
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb701
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb80
-rw-r--r--spec/services/groups/create_service_spec.rb22
-rw-r--r--spec/services/projects/upload_service_spec.rb14
-rw-r--r--spec/support/carrierwave.rb4
-rw-r--r--spec/support/matchers/markdown_matchers.rb7
-rw-r--r--spec/tasks/gitlab/info_rake_spec.rb37
-rw-r--r--spec/uploaders/file_uploader_spec.rb23
-rw-r--r--spec/uploaders/records_uploads_spec.rb97
-rw-r--r--spec/uploaders/uploader_helper_spec.rb12
-rw-r--r--spec/workers/stuck_ci_builds_worker_spec.rb57
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb129
-rw-r--r--spec/workers/upload_checksum_worker_spec.rb19
71 files changed, 2230 insertions, 683 deletions
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 699c6f77cec..cd6961a7bd5 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -35,6 +35,19 @@ describe Projects::UploadsController do
expect(response.body).to match '\"alt\":\"rails_sample\"'
expect(response.body).to match "\"url\":\"/uploads"
end
+
+ # NOTE: This is as close as we're getting to an Integration test for this
+ # behavior. We're avoiding a proper Feature test because those should be
+ # testing things entirely user-facing, which the Upload model is very much
+ # not.
+ it 'creates a corresponding Upload record' do
+ upload = Upload.last
+
+ aggregate_failures do
+ expect(upload).to exist
+ expect(upload.model).to eq project
+ end
+ end
end
context 'with valid non-image file' do
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 202759664a0..a1ec41322ad 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -158,14 +158,6 @@ describe ProjectsController do
expect(response).to render_template('_activity')
end
- it "renders the readme view" do
- allow(controller).to receive(:current_user).and_return(user)
- allow(user).to receive(:project_view).and_return('readme')
-
- get :show, namespace_id: public_project.namespace, id: public_project
- expect(response).to render_template('_readme')
- end
-
it "renders the files view" do
allow(controller).to receive(:current_user).and_return(user)
allow(user).to receive(:project_view).and_return('files')
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index cabe128acf7..279583c2c44 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -57,7 +57,7 @@ FactoryGirl.define do
end
trait :manual do
- status 'skipped'
+ status 'manual'
self.when 'manual'
end
@@ -71,8 +71,11 @@ FactoryGirl.define do
allow_failure true
end
+ trait :ignored do
+ allowed_to_fail
+ end
+
trait :playable do
- skipped
manual
end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 756b341ecba..169590deb8e 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -35,6 +35,10 @@ FactoryGirl.define do
status 'created'
end
+ trait :manual do
+ status 'manual'
+ end
+
after(:build) do |build, evaluator|
build.project = build.pipeline.project
end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index b740e191f48..55e10a1a89b 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -57,7 +57,7 @@ describe "User Feed", feature: true do
end
it 'has XHTML summaries in notes' do
- expect(body).to match /Bug confirmed <img[^>]*\/>/
+ expect(body).to match /Bug confirmed <gl-emoji[^>]*>/
end
it 'has XHTML summaries in merge request descriptions' do
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index fec86128d03..4638812b2d9 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -252,7 +252,7 @@ describe 'Copy as GFM', feature: true, js: true do
<<-GFM.strip_heredoc
<a name="named-anchor"></a>
-
+
<sub>sub</sub>
<dl>
@@ -275,6 +275,10 @@ describe 'Copy as GFM', feature: true, js: true do
<rp>rp</rp>
<abbr>abbr</abbr>
+
+ <summary>summary</summary>
+
+ <details>details</details>
GFM
)
diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb
index c203e1f20c1..65373e3f77d 100644
--- a/spec/features/environment_spec.rb
+++ b/spec/features/environment_spec.rb
@@ -13,7 +13,7 @@ feature 'Environment', :feature do
feature 'environment details page' do
given!(:environment) { create(:environment, project: project) }
given!(:deployment) { }
- given!(:manual) { }
+ given!(:action) { }
before do
visit_environment(environment)
@@ -69,17 +69,23 @@ feature 'Environment', :feature do
end
context 'with manual action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
+ given(:action) do
+ create(:ci_build, :manual, pipeline: pipeline,
+ name: 'deploy to production')
+ end
scenario 'does show a play button' do
- expect(page).to have_link(manual.name.humanize)
+ expect(page).to have_link(action.name.humanize)
end
scenario 'does allow to play manual action' do
- expect(manual).to be_skipped
- expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
- expect(page).to have_content(manual.name)
- expect(manual.reload).to be_pending
+ expect(action).to be_manual
+
+ expect { click_link(action.name.humanize) }
+ .not_to change { Ci::Pipeline.count }
+
+ expect(page).to have_content(action.name)
+ expect(action.reload).to be_pending
end
context 'with external_url' do
@@ -130,8 +136,16 @@ feature 'Environment', :feature do
context 'when environment is available' do
context 'with stop action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
- given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+ given(:action) do
+ create(:ci_build, :manual, pipeline: pipeline,
+ name: 'close_app')
+ end
+
+ given(:deployment) do
+ create(:deployment, environment: environment,
+ deployable: build,
+ on_stop: 'close_app')
+ end
scenario 'does show stop button' do
expect(page).to have_link('Stop')
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index 78be7d36f47..25f31b423b8 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -12,7 +12,7 @@ feature 'Environments page', :feature, :js do
given!(:environment) { }
given!(:deployment) { }
- given!(:manual) { }
+ given!(:action) { }
before do
visit_environments(project)
@@ -90,7 +90,7 @@ feature 'Environments page', :feature, :js do
given(:pipeline) { create(:ci_pipeline, project: project) }
given(:build) { create(:ci_build, pipeline: pipeline) }
- given(:manual) do
+ given(:action) do
create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production')
end
@@ -102,19 +102,19 @@ feature 'Environments page', :feature, :js do
scenario 'does show a play button' do
find('.js-dropdown-play-icon-container').click
- expect(page).to have_content(manual.name.humanize)
+ expect(page).to have_content(action.name.humanize)
end
scenario 'does allow to play manual action', js: true do
- expect(manual).to be_skipped
+ expect(action).to be_manual
find('.js-dropdown-play-icon-container').click
- expect(page).to have_content(manual.name.humanize)
+ expect(page).to have_content(action.name.humanize)
- expect { click_link(manual.name.humanize) }
+ expect { click_link(action.name.humanize) }
.not_to change { Ci::Pipeline.count }
- expect(manual.reload).to be_pending
+ expect(action.reload).to be_pending
end
scenario 'does show build name and id' do
@@ -144,8 +144,15 @@ feature 'Environments page', :feature, :js do
end
context 'with stop action' do
- given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
- given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
+ given(:action) do
+ create(:ci_build, :manual, pipeline: pipeline, name: 'close_app')
+ end
+
+ given(:deployment) do
+ create(:deployment, environment: environment,
+ deployable: build,
+ on_stop: 'close_app')
+ end
scenario 'does show stop button' do
expect(page).to have_selector('.stop-env-link')
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 37b7c20239f..d243f9478bb 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -43,6 +43,44 @@ feature 'Group', feature: true do
expect(page).to have_namespace_error_message
end
end
+
+ describe 'Mattermost team creation' do
+ before do
+ allow(Settings.mattermost).to receive_messages(enabled: mattermost_enabled)
+
+ visit new_group_path
+ end
+
+ context 'Mattermost enabled' do
+ let(:mattermost_enabled) { true }
+
+ it 'displays a team creation checkbox' do
+ expect(page).to have_selector('#group_create_chat_team')
+ end
+
+ it 'checks the checkbox by default' do
+ expect(find('#group_create_chat_team')['checked']).to eq(true)
+ end
+
+ it 'updates the team URL on graph path update', :js do
+ out_span = find('span[data-bind-out="create_chat_team"]')
+
+ expect(out_span.text).to be_empty
+
+ fill_in('group_path', with: 'test-group')
+
+ expect(out_span.text).to eq('test-group')
+ end
+ end
+
+ context 'Mattermost disabled' do
+ let(:mattermost_enabled) { false }
+
+ it 'doesnt show a team creation checkbox if Mattermost not enabled' do
+ expect(page).not_to have_selector('#group_create_chat_team')
+ end
+ end
+ end
end
describe 'create a nested group' do
@@ -105,7 +143,7 @@ feature 'Group', feature: true do
visit path
- expect(page).to have_css('.group-home-desc > p > img')
+ expect(page).to have_css('.group-home-desc > p > gl-emoji')
end
it 'sanitizes unwanted tags' do
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 3ab3d2d4229..f424186cf30 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -25,14 +25,14 @@ describe 'Awards Emoji', feature: true do
end
it 'increments the thumbsdown emoji', js: true do
- find('[data-emoji="thumbsdown"]').click
+ find('[data-name="thumbsdown"]').click
wait_for_ajax
expect(thumbsdown_emoji).to have_text("1")
end
context 'click the thumbsup emoji' do
it 'increments the thumbsup emoji', js: true do
- find('[data-emoji="thumbsup"]').click
+ find('[data-name="thumbsup"]').click
wait_for_ajax
expect(thumbsup_emoji).to have_text("1")
end
@@ -44,7 +44,7 @@ describe 'Awards Emoji', feature: true do
context 'click the thumbsdown emoji' do
it 'increments the thumbsdown emoji', js: true do
- find('[data-emoji="thumbsdown"]').click
+ find('[data-name="thumbsdown"]').click
wait_for_ajax
expect(thumbsdown_emoji).to have_text("1")
end
@@ -123,9 +123,9 @@ describe 'Awards Emoji', feature: true do
end
unless status
- first('[data-emoji="smiley"]').click
+ first('[data-name="smiley"]').click
else
- find('[data-emoji="smiley"]').click
+ find('[data-name="smiley"]').click
end
wait_for_ajax
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index 741ca95f1ca..d4e0ef91856 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -3,6 +3,7 @@ require 'rails_helper'
describe 'New/edit issue', feature: true, js: true do
let!(:project) { create(:project) }
let!(:user) { create(:user)}
+ let!(:user2) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
let!(:label2) { create(:label, project: project) }
@@ -10,6 +11,7 @@ describe 'New/edit issue', feature: true, js: true do
before do
project.team << [user, :master]
+ project.team << [user2, :master]
login_as(user)
end
@@ -22,14 +24,23 @@ describe 'New/edit issue', feature: true, js: true do
fill_in 'issue_title', with: 'title'
fill_in 'issue_description', with: 'title'
+ expect(find('a', text: 'Assign to me')).to be_visible
click_button 'Assignee'
page.within '.dropdown-menu-user' do
- click_link user.name
+ click_link user2.name
end
+ expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user2.name
+ end
+ expect(find('a', text: 'Assign to me')).to be_visible
+
+ click_link 'Assign to me'
expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
end
+ expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
click_button 'Milestone'
page.within '.issue-milestone' do
@@ -94,6 +105,7 @@ describe 'New/edit issue', feature: true, js: true do
it 'allows user to update issue' do
expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ expect(find('a', text: 'Assign to me', visible: false)).not_to be_visible
page.within '.js-user-search' do
expect(page).to have_content user.name
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 32159559c37..894df13a2dc 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -115,6 +115,14 @@ describe 'GitLab Markdown', feature: true do
expect(doc).to have_selector('span:contains("span tag")')
end
+ it 'permits details elements' do
+ expect(doc).to have_selector('details:contains("Hiding the details")')
+ end
+
+ it 'permits summary elements' do
+ expect(doc).to have_selector('details summary:contains("collapsible")')
+ end
+
it 'permits style attribute in th elements' do
aggregate_failures do
expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
index 7594cbf54e8..1ecdb8b5983 100644
--- a/spec/features/merge_requests/form_spec.rb
+++ b/spec/features/merge_requests/form_spec.rb
@@ -4,12 +4,14 @@ describe 'New/edit merge request', feature: true, js: true do
let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:fork_project) { create(:project, forked_from_project: project) }
let!(:user) { create(:user)}
+ let!(:user2) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
let!(:label2) { create(:label, project: project) }
before do
project.team << [user, :master]
+ project.team << [user2, :master]
end
context 'owned projects' do
@@ -33,8 +35,14 @@ describe 'New/edit merge request', feature: true, js: true do
it 'creates new merge request' do
click_button 'Assignee'
page.within '.dropdown-menu-user' do
- click_link user.name
+ click_link user2.name
+ end
+ expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user2.name
end
+
+ click_link 'Assign to me'
expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index c30d38b6508..3a1240f95b5 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -18,7 +18,7 @@ feature 'Project', feature: true do
it 'passes through html-pipeline' do
project.update_attribute(:description, 'This project is the :poop:')
visit path
- expect(page).to have_css('.project-home-desc > p > img')
+ expect(page).to have_css('.project-home-desc > p > gl-emoji')
end
it 'sanitizes unwanted tags' do
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index f3e7c2d1a9f..0cdbc32431d 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -79,6 +79,11 @@ As permissive as it is, we've allowed even more stuff:
<span>span tag</span>
+<details>
+<summary>Summary lines are collapsible:</summary>
+Hiding the details until expanded.
+</details>
+
<a href="#" rel="bookmark">This is a link with a defined rel attribute, which should be removed</a>
<a href="javascript:alert('Hi')">This is a link trying to be sneaky. It gets its link removed entirely.</a>
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index b8ec3521edb..9ffd4b9371c 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do
it 'replaces commit message with emoji to link' do
actual = link_to_gfm(':book:Book', '/foo')
expect(actual).
- to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://#{Gitlab.config.gitlab.host}/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
+ to eq '<gl-emoji data-name="book" data-unicode-version="6.0">📖</gl-emoji><a href="/foo">Book</a>'
end
end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index e5826f9c29f..f4b1d777203 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,11 +1,11 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, max-len */
-/* global AwardsHandler */
-require('~/awards_handler');
-require('./fixtures/emoji_menu');
+require('es6-promise').polyfill();
+
+const AwardsHandler = require('~/awards_handler');
(function() {
- var awardsHandler, lazyAssert, urlRoot;
+ var awardsHandler, lazyAssert, urlRoot, openAndWaitForEmojiMenu;
awardsHandler = null;
@@ -13,14 +13,6 @@ require('./fixtures/emoji_menu');
window.gon || (window.gon = {});
- gl.emojiAliases = function() {
- return {
- '+1': 'thumbsup',
- '-1': 'thumbsdown'
- };
- };
-
- gon.award_menu_url = '/emojis';
urlRoot = gon.relative_url_root;
lazyAssert = function(done, assertFn) {
@@ -32,22 +24,40 @@ require('./fixtures/emoji_menu');
};
describe('AwardsHandler', function() {
- preloadFixtures('issues/open-issue.html.raw');
+ preloadFixtures('issues/issue_with_comment.html.raw');
beforeEach(function() {
- loadFixtures('issues/open-issue.html.raw');
+ loadFixtures('issues/issue_with_comment.html.raw');
awardsHandler = new AwardsHandler;
spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) {
return function(url, emoji, cb) {
return cb();
};
})(this));
- spyOn(jQuery, 'get').and.callFake(function(req, cb) {
- return cb(window.emojiMenu);
- });
+
+ let isEmojiMenuBuilt = false;
+ openAndWaitForEmojiMenu = function() {
+ return new Promise((resolve, reject) => {
+ if (isEmojiMenuBuilt) {
+ resolve();
+ } else {
+ $('.js-add-award').eq(0).click();
+ const $menu = $('.emoji-menu');
+ $menu.one('build-emoji-menu-finish', () => {
+ isEmojiMenuBuilt = true;
+ resolve();
+ });
+
+ // Fail after 1 second
+ setTimeout(reject, 1000);
+ }
+ });
+ };
});
afterEach(function() {
// restore original url root value
gon.relative_url_root = urlRoot;
+
+ awardsHandler.destroy();
});
describe('::showEmojiMenu', function() {
it('should show emoji menu when Add emoji button clicked', function(done) {
@@ -62,10 +72,9 @@ require('./fixtures/emoji_menu');
});
});
it('should also show emoji menu for the smiley icon in notes', function(done) {
- $('.note-action-button').click();
+ $('.js-add-award.note-action-button').click();
return lazyAssert(done, function() {
- var $emojiMenu;
- $emojiMenu = $('.emoji-menu');
+ var $emojiMenu = $('.emoji-menu');
return expect($emojiMenu.length).toBe(1);
});
});
@@ -86,7 +95,7 @@ require('./fixtures/emoji_menu');
var $emojiButton, $votesBlock;
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
- $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ $emojiButton = $votesBlock.find('[data-name=heart]');
expect($emojiButton.length).toBe(1);
expect($emojiButton.next('.js-counter').text()).toBe('1');
return expect($votesBlock.hasClass('hidden')).toBe(false);
@@ -96,14 +105,14 @@ require('./fixtures/emoji_menu');
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
- $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ $emojiButton = $votesBlock.find('[data-name=heart]');
return expect($emojiButton.length).toBe(0);
});
return it('should decrement the emoji counter', function() {
var $emojiButton, $votesBlock;
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
- $emojiButton = $votesBlock.find('[data-emoji=heart]');
+ $emojiButton = $votesBlock.find('[data-name=heart]');
$emojiButton.next('.js-counter').text(5);
awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false);
expect($emojiButton.length).toBe(1);
@@ -120,8 +129,8 @@ require('./fixtures/emoji_menu');
var $thumbsDownEmoji, $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
- $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
- $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent();
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
+ $thumbsDownEmoji = $votesBlock.find('[data-name=thumbsdown]').parent();
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
expect($thumbsUpEmoji.hasClass('active')).toBe(true);
expect($thumbsDownEmoji.hasClass('active')).toBe(false);
@@ -138,9 +147,9 @@ require('./fixtures/emoji_menu');
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
awardsHandler.addAward($votesBlock, awardUrl, 'fire', false);
- expect($votesBlock.find('[data-emoji=fire]').length).toBe(1);
- awardsHandler.removeEmoji($votesBlock.find('[data-emoji=fire]').closest('button'));
- return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0);
+ expect($votesBlock.find('[data-name=fire]').length).toBe(1);
+ awardsHandler.removeEmoji($votesBlock.find('[data-name=fire]').closest('button'));
+ return expect($votesBlock.find('[data-name=fire]').length).toBe(0);
});
});
describe('::addYouToUserList', function() {
@@ -148,7 +157,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
- $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
@@ -158,7 +167,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
- $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
$thumbsUpEmoji.tooltip();
@@ -170,7 +179,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
- $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
@@ -181,7 +190,7 @@ require('./fixtures/emoji_menu');
var $thumbsUpEmoji, $votesBlock, awardUrl;
awardUrl = awardsHandler.getAwardUrl();
$votesBlock = $('.js-awards-block').eq(0);
- $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent();
+ $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').parent();
$thumbsUpEmoji.attr('data-title', 'You and sam');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
@@ -190,42 +199,58 @@ require('./fixtures/emoji_menu');
});
});
describe('search', function() {
- return it('should filter the emoji', function() {
- $('.js-add-award').eq(0).click();
- expect($('[data-emoji=angel]').is(':visible')).toBe(true);
- expect($('[data-emoji=anger]').is(':visible')).toBe(true);
- $('#emoji_search').val('ali').trigger('keyup');
- expect($('[data-emoji=angel]').is(':visible')).toBe(false);
- expect($('[data-emoji=anger]').is(':visible')).toBe(false);
- return expect($('[data-emoji=alien]').is(':visible')).toBe(true);
+ return it('should filter the emoji', function(done) {
+ return openAndWaitForEmojiMenu()
+ .then(() => {
+ expect($('[data-name=angel]').is(':visible')).toBe(true);
+ expect($('[data-name=anger]').is(':visible')).toBe(true);
+ $('#emoji_search').val('ali').trigger('input');
+ expect($('[data-name=angel]').is(':visible')).toBe(false);
+ expect($('[data-name=anger]').is(':visible')).toBe(false);
+ expect($('[data-name=alien]').is(':visible')).toBe(true);
+ })
+ .then(done)
+ .catch(() => {
+ done.fail('Failed to open and build emoji menu');
+ });
});
});
- return describe('emoji menu', function() {
- var openEmojiMenuAndAddEmoji, selector;
- selector = '[data-emoji=sunglasses]';
- openEmojiMenuAndAddEmoji = function() {
- var $block, $emoji, $menu;
- $('.js-add-award').eq(0).click();
- $menu = $('.emoji-menu');
- $block = $('.js-awards-block');
- $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + selector);
- expect($emoji.length).toBe(1);
- expect($block.find(selector).length).toBe(0);
- $emoji.click();
- expect($menu.hasClass('.is-visible')).toBe(false);
- return expect($block.find(selector).length).toBe(1);
+ describe('emoji menu', function() {
+ const emojiSelector = '[data-name="sunglasses"]';
+ const openEmojiMenuAndAddEmoji = function() {
+ return openAndWaitForEmojiMenu()
+ .then(() => {
+ const $menu = $('.emoji-menu');
+ const $block = $('.js-awards-block');
+ const $emoji = $menu.find('.emoji-menu-list:not(.frequent-emojis) ' + emojiSelector);
+
+ expect($emoji.length).toBe(1);
+ expect($block.find(emojiSelector).length).toBe(0);
+ $emoji.click();
+ expect($menu.hasClass('.is-visible')).toBe(false);
+ expect($block.find(emojiSelector).length).toBe(1);
+ });
};
- it('should add selected emoji to awards block', function() {
- return openEmojiMenuAndAddEmoji();
+ it('should add selected emoji to awards block', function(done) {
+ return openEmojiMenuAndAddEmoji()
+ .then(done)
+ .catch(() => {
+ done.fail('Failed to open and build emoji menu');
+ });
});
- return it('should remove already selected emoji', function() {
- var $block, $emoji;
- openEmojiMenuAndAddEmoji();
- $('.js-add-award').eq(0).click();
- $block = $('.js-awards-block');
- $emoji = $('.emoji-menu').find(".emoji-menu-list:not(.frequent-emojis) " + selector);
- $emoji.click();
- return expect($block.find(selector).length).toBe(0);
+ it('should remove already selected emoji', function(done) {
+ return openEmojiMenuAndAddEmoji()
+ .then(() => {
+ $('.js-add-award').eq(0).click();
+ const $block = $('.js-awards-block');
+ const $emoji = $('.emoji-menu').find(`.emoji-menu-list:not(.frequent-emojis) ${emojiSelector}`);
+ $emoji.click();
+ expect($block.find(emojiSelector).length).toBe(0);
+ })
+ .then(done)
+ .catch((err) => {
+ done.fail('Failed to open and build emoji menu');
+ });
});
});
});
diff --git a/spec/javascripts/behaviors/bind_in_out_spec.js b/spec/javascripts/behaviors/bind_in_out_spec.js
new file mode 100644
index 00000000000..dd9ab33289f
--- /dev/null
+++ b/spec/javascripts/behaviors/bind_in_out_spec.js
@@ -0,0 +1,189 @@
+import BindInOut from '~/behaviors/bind_in_out';
+import ClassSpecHelper from '../helpers/class_spec_helper';
+
+describe('BindInOut', function () {
+ describe('.constructor', function () {
+ beforeEach(function () {
+ this.in = {};
+ this.out = {};
+
+ this.bindInOut = new BindInOut(this.in, this.out);
+ });
+
+ it('should set .in', function () {
+ expect(this.bindInOut.in).toBe(this.in);
+ });
+
+ it('should set .out', function () {
+ expect(this.bindInOut.out).toBe(this.out);
+ });
+
+ it('should set .eventWrapper', function () {
+ expect(this.bindInOut.eventWrapper).toEqual({});
+ });
+
+ describe('if .in is an input', function () {
+ beforeEach(function () {
+ this.bindInOut = new BindInOut({ tagName: 'INPUT' });
+ });
+
+ it('should set .eventType to keyup ', function () {
+ expect(this.bindInOut.eventType).toEqual('keyup');
+ });
+ });
+
+ describe('if .in is a textarea', function () {
+ beforeEach(function () {
+ this.bindInOut = new BindInOut({ tagName: 'TEXTAREA' });
+ });
+
+ it('should set .eventType to keyup ', function () {
+ expect(this.bindInOut.eventType).toEqual('keyup');
+ });
+ });
+
+ describe('if .in is not an input or textarea', function () {
+ beforeEach(function () {
+ this.bindInOut = new BindInOut({ tagName: 'SELECT' });
+ });
+
+ it('should set .eventType to change ', function () {
+ expect(this.bindInOut.eventType).toEqual('change');
+ });
+ });
+ });
+
+ describe('.addEvents', function () {
+ beforeEach(function () {
+ this.in = jasmine.createSpyObj('in', ['addEventListener']);
+
+ this.bindInOut = new BindInOut(this.in);
+
+ this.addEvents = this.bindInOut.addEvents();
+ });
+
+ it('should set .eventWrapper.updateOut', function () {
+ expect(this.bindInOut.eventWrapper.updateOut).toEqual(jasmine.any(Function));
+ });
+
+ it('should call .addEventListener', function () {
+ expect(this.in.addEventListener)
+ .toHaveBeenCalledWith(
+ this.bindInOut.eventType,
+ this.bindInOut.eventWrapper.updateOut,
+ );
+ });
+
+ it('should return the instance', function () {
+ expect(this.addEvents).toBe(this.bindInOut);
+ });
+ });
+
+ describe('.updateOut', function () {
+ beforeEach(function () {
+ this.in = { value: 'the-value' };
+ this.out = { textContent: 'not-the-value' };
+
+ this.bindInOut = new BindInOut(this.in, this.out);
+
+ this.updateOut = this.bindInOut.updateOut();
+ });
+
+ it('should set .out.textContent to .in.value', function () {
+ expect(this.out.textContent).toBe(this.in.value);
+ });
+
+ it('should return the instance', function () {
+ expect(this.updateOut).toBe(this.bindInOut);
+ });
+ });
+
+ describe('.removeEvents', function () {
+ beforeEach(function () {
+ this.in = jasmine.createSpyObj('in', ['removeEventListener']);
+ this.updateOut = () => {};
+
+ this.bindInOut = new BindInOut(this.in);
+ this.bindInOut.eventWrapper.updateOut = this.updateOut;
+
+ this.removeEvents = this.bindInOut.removeEvents();
+ });
+
+ it('should call .removeEventListener', function () {
+ expect(this.in.removeEventListener)
+ .toHaveBeenCalledWith(
+ this.bindInOut.eventType,
+ this.updateOut,
+ );
+ });
+
+ it('should return the instance', function () {
+ expect(this.removeEvents).toBe(this.bindInOut);
+ });
+ });
+
+ describe('.initAll', function () {
+ beforeEach(function () {
+ this.ins = [0, 1, 2];
+ this.instances = [];
+
+ spyOn(document, 'querySelectorAll').and.returnValue(this.ins);
+ spyOn(Array.prototype, 'map').and.callThrough();
+ spyOn(BindInOut, 'init');
+
+ this.initAll = BindInOut.initAll();
+ });
+
+ ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'initAll');
+
+ it('should call .querySelectorAll', function () {
+ expect(document.querySelectorAll).toHaveBeenCalledWith('*[data-bind-in]');
+ });
+
+ it('should call .map', function () {
+ expect(Array.prototype.map).toHaveBeenCalledWith(jasmine.any(Function));
+ });
+
+ it('should call .init for each element', function () {
+ expect(BindInOut.init.calls.count()).toEqual(3);
+ });
+
+ it('should return an array of instances', function () {
+ expect(this.initAll).toEqual(jasmine.any(Array));
+ });
+ });
+
+ describe('.init', function () {
+ beforeEach(function () {
+ spyOn(BindInOut.prototype, 'addEvents').and.callFake(function () { return this; });
+ spyOn(BindInOut.prototype, 'updateOut').and.callFake(function () { return this; });
+
+ this.init = BindInOut.init({}, {});
+ });
+
+ ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'init');
+
+ it('should call .addEvents', function () {
+ expect(BindInOut.prototype.addEvents).toHaveBeenCalled();
+ });
+
+ it('should call .updateOut', function () {
+ expect(BindInOut.prototype.updateOut).toHaveBeenCalled();
+ });
+
+ describe('if no anOut is provided', function () {
+ beforeEach(function () {
+ this.anIn = { dataset: { bindIn: 'the-data-bind-in' } };
+
+ spyOn(document, 'querySelector');
+
+ BindInOut.init(this.anIn);
+ });
+
+ it('should call .querySelector', function () {
+ expect(document.querySelector)
+ .toHaveBeenCalledWith(`*[data-bind-out="${this.anIn.dataset.bindIn}"]`);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/build_spec.js b/spec/javascripts/build_spec.js
index 0bd50588f5a..fe7f3d2e9c4 100644
--- a/spec/javascripts/build_spec.js
+++ b/spec/javascripts/build_spec.js
@@ -9,12 +9,6 @@ require('vendor/jquery.nicescroll');
describe('Build', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`;
- // see spec/factories/ci/builds.rb
- const BUILD_TRACE = 'BUILD TRACE';
- // see lib/ci/ansi2html.rb
- const INITIAL_BUILD_TRACE_STATE = window.btoa(JSON.stringify({
- offset: BUILD_TRACE.length, n_open_tags: 0, fg_color: null, bg_color: null, style_mask: 0,
- }));
preloadFixtures('builds/build-with-artifacts.html.raw');
@@ -42,7 +36,7 @@ describe('Build', () => {
expect(this.build.buildUrl).toBe(`${BUILD_URL}.json`);
expect(this.build.buildStatus).toBe('success');
expect(this.build.buildStage).toBe('test');
- expect(this.build.state).toBe(INITIAL_BUILD_TRACE_STATE);
+ expect(this.build.state).toBe('');
});
it('only shows the jobs matching the current stage', () => {
@@ -108,7 +102,7 @@ describe('Build', () => {
expect($.ajax.calls.count()).toBe(2);
let [{ url, dataType, success, context }] = $.ajax.calls.argsFor(1);
expect(url).toBe(
- `${BUILD_URL}/trace.json?state=${encodeURIComponent(INITIAL_BUILD_TRACE_STATE)}`,
+ `${BUILD_URL}/trace.json?state=`,
);
expect(dataType).toBe('json');
expect(success).toEqual(jasmine.any(Function));
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
deleted file mode 100644
index a50812d9517..00000000000
--- a/spec/javascripts/fixtures/emoji_menu.js
+++ /dev/null
@@ -1,4 +0,0 @@
-/* eslint-disable space-before-function-paren */
-(function() {
- window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>";
-}).call(window);
diff --git a/spec/javascripts/gl_emoji_spec.js b/spec/javascripts/gl_emoji_spec.js
new file mode 100644
index 00000000000..e94e220b19f
--- /dev/null
+++ b/spec/javascripts/gl_emoji_spec.js
@@ -0,0 +1,367 @@
+
+require('~/extensions/string');
+require('~/extensions/array');
+
+const glEmoji = require('~/behaviors/gl_emoji');
+
+const glEmojiTag = glEmoji.glEmojiTag;
+const isEmojiUnicodeSupported = glEmoji.isEmojiUnicodeSupported;
+const isFlagEmoji = glEmoji.isFlagEmoji;
+const isKeycapEmoji = glEmoji.isKeycapEmoji;
+const isSkinToneComboEmoji = glEmoji.isSkinToneComboEmoji;
+const isHorceRacingSkinToneComboEmoji = glEmoji.isHorceRacingSkinToneComboEmoji;
+const isPersonZwjEmoji = glEmoji.isPersonZwjEmoji;
+
+const emptySupportMap = {
+ personZwj: false,
+ horseRacing: false,
+ flag: false,
+ skinToneModifier: false,
+ '9.0': false,
+ '8.0': false,
+ '7.0': false,
+ 6.1: false,
+ '6.0': false,
+ 5.2: false,
+ 5.1: false,
+ 4.1: false,
+ '4.0': false,
+ 3.2: false,
+ '3.0': false,
+ 1.1: false,
+};
+
+const emojiFixtureMap = {
+ bomb: {
+ name: 'bomb',
+ moji: '💣',
+ unicodeVersion: '6.0',
+ },
+ construction_worker_tone5: {
+ name: 'construction_worker_tone5',
+ moji: '👷🏿',
+ unicodeVersion: '8.0',
+ },
+ five: {
+ name: 'five',
+ moji: '5️⃣',
+ unicodeVersion: '3.0',
+ },
+};
+
+function markupToDomElement(markup) {
+ const div = document.createElement('div');
+ div.innerHTML = markup;
+ return div.firstElementChild;
+}
+
+function testGlEmojiImageFallback(element, name, src) {
+ expect(element.tagName.toLowerCase()).toBe('img');
+ expect(element.getAttribute('src')).toBe(src);
+ expect(element.getAttribute('title')).toBe(`:${name}:`);
+ expect(element.getAttribute('alt')).toBe(`:${name}:`);
+}
+
+const defaults = {
+ forceFallback: false,
+ sprite: false,
+};
+
+function testGlEmojiElement(element, name, unicodeVersion, unicodeMoji, options = {}) {
+ const opts = Object.assign({}, defaults, options);
+ expect(element.tagName.toLowerCase()).toBe('gl-emoji');
+ expect(element.dataset.name).toBe(name);
+ expect(element.dataset.fallbackSrc.length).toBeGreaterThan(0);
+ expect(element.dataset.unicodeVersion).toBe(unicodeVersion);
+
+ const fallbackSpriteClass = `emoji-${name}`;
+ if (opts.sprite) {
+ expect(element.dataset.fallbackSpriteClass).toBe(fallbackSpriteClass);
+ }
+
+ if (opts.forceFallback && opts.sprite) {
+ expect(element.getAttribute('class')).toBe(`emoji-icon ${fallbackSpriteClass}`);
+ }
+
+ if (opts.forceFallback && !opts.sprite) {
+ // Check for image fallback
+ testGlEmojiImageFallback(element.firstElementChild, name, element.dataset.fallbackSrc);
+ } else {
+ // Otherwise make sure things are still unicode text
+ expect(element.textContent.trim()).toBe(unicodeMoji);
+ }
+}
+
+describe('gl_emoji', () => {
+ describe('glEmojiTag', () => {
+ it('bomb emoji', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name);
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ );
+ });
+
+ it('bomb emoji with image fallback', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+ forceFallback: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ forceFallback: true,
+ },
+ );
+ });
+
+ it('bomb emoji with sprite fallback readiness', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+ sprite: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ sprite: true,
+ },
+ );
+ });
+ it('bomb emoji with sprite fallback', () => {
+ const emojiKey = 'bomb';
+ const markup = glEmojiTag(emojiFixtureMap[emojiKey].name, {
+ forceFallback: true,
+ sprite: true,
+ });
+ const glEmojiElement = markupToDomElement(markup);
+ testGlEmojiElement(
+ glEmojiElement,
+ emojiFixtureMap[emojiKey].name,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ emojiFixtureMap[emojiKey].moji,
+ {
+ forceFallback: true,
+ sprite: true,
+ },
+ );
+ });
+ });
+
+ describe('isFlagEmoji', () => {
+ it('should detect flag_ac', () => {
+ expect(isFlagEmoji('🇦🇨')).toBeTruthy();
+ });
+ it('should detect flag_us', () => {
+ expect(isFlagEmoji('🇺🇸')).toBeTruthy();
+ });
+ it('should detect flag_zw', () => {
+ expect(isFlagEmoji('🇿🇼')).toBeTruthy();
+ });
+ it('should not detect flags', () => {
+ expect(isFlagEmoji('🎏')).toBeFalsy();
+ });
+ it('should not detect triangular_flag_on_post', () => {
+ expect(isFlagEmoji('🚩')).toBeFalsy();
+ });
+ it('should not detect single letter', () => {
+ expect(isFlagEmoji('🇦')).toBeFalsy();
+ });
+ it('should not detect >2 letters', () => {
+ expect(isFlagEmoji('🇦🇧🇨')).toBeFalsy();
+ });
+ });
+
+ describe('isKeycapEmoji', () => {
+ it('should detect one(keycap)', () => {
+ expect(isKeycapEmoji('1️⃣')).toBeTruthy();
+ });
+ it('should detect nine(keycap)', () => {
+ expect(isKeycapEmoji('9️⃣')).toBeTruthy();
+ });
+ it('should not detect ten(keycap)', () => {
+ expect(isKeycapEmoji('🔟')).toBeFalsy();
+ });
+ it('should not detect hash(keycap)', () => {
+ expect(isKeycapEmoji('#⃣')).toBeFalsy();
+ });
+ });
+
+ describe('isSkinToneComboEmoji', () => {
+ it('should detect hand_splayed_tone5', () => {
+ expect(isSkinToneComboEmoji('🖐🏿')).toBeTruthy();
+ });
+ it('should not detect hand_splayed', () => {
+ expect(isSkinToneComboEmoji('🖐')).toBeFalsy();
+ });
+ it('should detect lifter_tone1', () => {
+ expect(isSkinToneComboEmoji('🏋🏻')).toBeTruthy();
+ });
+ it('should not detect lifter', () => {
+ expect(isSkinToneComboEmoji('🏋')).toBeFalsy();
+ });
+ it('should detect rowboat_tone4', () => {
+ expect(isSkinToneComboEmoji('🚣🏾')).toBeTruthy();
+ });
+ it('should not detect rowboat', () => {
+ expect(isSkinToneComboEmoji('🚣')).toBeFalsy();
+ });
+ it('should not detect individual tone emoji', () => {
+ expect(isSkinToneComboEmoji('🏻')).toBeFalsy();
+ });
+ });
+
+ describe('isHorceRacingSkinToneComboEmoji', () => {
+ it('should detect horse_racing_tone2', () => {
+ expect(isHorceRacingSkinToneComboEmoji('🏇🏼')).toBeTruthy();
+ });
+ it('should not detect horse_racing', () => {
+ expect(isHorceRacingSkinToneComboEmoji('🏇')).toBeFalsy();
+ });
+ });
+
+ describe('isPersonZwjEmoji', () => {
+ it('should detect couple_mm', () => {
+ expect(isPersonZwjEmoji('👨‍❤️‍👨')).toBeTruthy();
+ });
+ it('should not detect couple_with_heart', () => {
+ expect(isPersonZwjEmoji('💑')).toBeFalsy();
+ });
+ it('should not detect couplekiss', () => {
+ expect(isPersonZwjEmoji('💏')).toBeFalsy();
+ });
+ it('should detect family_mmb', () => {
+ expect(isPersonZwjEmoji('👨‍👨‍👦')).toBeTruthy();
+ });
+ it('should detect family_mwgb', () => {
+ expect(isPersonZwjEmoji('👨‍👩‍👧‍👦')).toBeTruthy();
+ });
+ it('should not detect family', () => {
+ expect(isPersonZwjEmoji('👪')).toBeFalsy();
+ });
+ it('should detect kiss_ww', () => {
+ expect(isPersonZwjEmoji('👩‍❤️‍💋‍👩')).toBeTruthy();
+ });
+ it('should not detect girl', () => {
+ expect(isPersonZwjEmoji('👧')).toBeFalsy();
+ });
+ it('should not detect girl_tone5', () => {
+ expect(isPersonZwjEmoji('👧🏿')).toBeFalsy();
+ });
+ it('should not detect man', () => {
+ expect(isPersonZwjEmoji('👨')).toBeFalsy();
+ });
+ it('should not detect woman', () => {
+ expect(isPersonZwjEmoji('👩')).toBeFalsy();
+ });
+ });
+
+ describe('isEmojiUnicodeSupported', () => {
+ it('bomb(6.0) with 6.0 support', () => {
+ const emojiKey = 'bomb';
+ const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+ '6.0': true,
+ });
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeTruthy();
+ });
+
+ it('bomb(6.0) without 6.0 support', () => {
+ const emojiKey = 'bomb';
+ const unicodeSupportMap = emptySupportMap;
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeFalsy();
+ });
+
+ it('bomb(6.0) without 6.0 but with 9.0 support', () => {
+ const emojiKey = 'bomb';
+ const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+ '9.0': true,
+ });
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeFalsy();
+ });
+
+ it('construction_worker_tone5(8.0) without skin tone modifier support', () => {
+ const emojiKey = 'construction_worker_tone5';
+ const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+ skinToneModifier: false,
+ '9.0': true,
+ '8.0': true,
+ '7.0': true,
+ 6.1: true,
+ '6.0': true,
+ 5.2: true,
+ 5.1: true,
+ 4.1: true,
+ '4.0': true,
+ 3.2: true,
+ '3.0': true,
+ 1.1: true,
+ });
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeFalsy();
+ });
+
+ it('use native keycap on >=57 chrome', () => {
+ const emojiKey = 'five';
+ const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+ '3.0': true,
+ meta: {
+ isChrome: true,
+ chromeVersion: 57,
+ },
+ });
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeTruthy();
+ });
+
+ it('fallback keycap on <57 chrome', () => {
+ const emojiKey = 'five';
+ const unicodeSupportMap = Object.assign({}, emptySupportMap, {
+ '3.0': true,
+ meta: {
+ isChrome: true,
+ chromeVersion: 50,
+ },
+ });
+ const isSupported = isEmojiUnicodeSupported(
+ unicodeSupportMap,
+ emojiFixtureMap[emojiKey].moji,
+ emojiFixtureMap[emojiKey].unicodeVersion,
+ );
+ expect(isSupported).toBeFalsy();
+ });
+ });
+});
diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
index c8e62f528df..707212e07fd 100644
--- a/spec/lib/banzai/filter/emoji_filter_spec.rb
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -14,12 +14,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do
it 'replaces supported name emoji' do
doc = filter('<p>:heart:</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
+ expect(doc.css('gl-emoji').first.text).to eq '❤'
end
it 'replaces supported unicode emoji' do
doc = filter('<p>❤️</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png'
+ expect(doc.css('gl-emoji').first.text).to eq '❤'
end
it 'ignores unsupported emoji' do
@@ -30,152 +30,78 @@ describe Banzai::Filter::EmojiFilter, lib: true do
it 'correctly encodes the URL' do
doc = filter('<p>:+1:</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
+ expect(doc.css('gl-emoji').first.text).to eq '👍'
end
it 'correctly encodes unicode to the URL' do
doc = filter('<p>👍</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png'
+ expect(doc.css('gl-emoji').first.text).to eq '👍'
end
it 'matches at the start of a string' do
doc = filter(':+1:')
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'unicode matches at the start of a string' do
doc = filter("'👍'")
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'matches at the end of a string' do
doc = filter('This gets a :-1:')
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'unicode matches at the end of a string' do
doc = filter('This gets a 👍')
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'matches with adjacent text' do
doc = filter('+1 (:+1:)')
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'unicode matches with adjacent text' do
doc = filter('+1 (👍)')
- expect(doc.css('img').size).to eq 1
+ expect(doc.css('gl-emoji').size).to eq 1
end
it 'matches multiple emoji in a row' do
doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
- expect(doc.css('img').size).to eq 3
+ expect(doc.css('gl-emoji').size).to eq 3
end
it 'unicode matches multiple emoji in a row' do
doc = filter("'🙈🙉🙊'")
- expect(doc.css('img').size).to eq 3
+ expect(doc.css('gl-emoji').size).to eq 3
end
it 'mixed matches multiple emoji in a row' do
doc = filter("'🙈:see_no_evil:🙉:hear_no_evil:🙊:speak_no_evil:'")
- expect(doc.css('img').size).to eq 6
+ expect(doc.css('gl-emoji').size).to eq 6
end
- it 'has a title attribute' do
+ it 'has a data-name attribute' do
doc = filter(':-1:')
- expect(doc.css('img').first.attr('title')).to eq ':-1:'
+ expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown'
end
- it 'unicode has a title attribute' do
- doc = filter("'👎'")
- expect(doc.css('img').first.attr('title')).to eq ':thumbsdown:'
- end
-
- it 'has an alt attribute' do
+ it 'has a data-unicode-version attribute' do
doc = filter(':-1:')
- expect(doc.css('img').first.attr('alt')).to eq ':-1:'
- end
-
- it 'unicode has an alt attribute' do
- doc = filter("'👎'")
- expect(doc.css('img').first.attr('alt')).to eq ':thumbsdown:'
- end
-
- it 'has an align attribute' do
- doc = filter(':8ball:')
- expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
- end
-
- it 'unicode has an align attribute' do
- doc = filter("'🎱'")
- expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
- end
-
- it 'has an emoji class' do
- doc = filter(':cat:')
- expect(doc.css('img').first.attr('class')).to eq 'emoji'
- end
-
- it 'unicode has an emoji class' do
- doc = filter("'🐱'")
- expect(doc.css('img').first.attr('class')).to eq 'emoji'
- end
-
- it 'has height and width attributes' do
- doc = filter(':dog:')
- img = doc.css('img').first
-
- expect(img.attr('width')).to eq '20'
- expect(img.attr('height')).to eq '20'
- end
-
- it 'unicode has height and width attributes' do
- doc = filter("'🐶'")
- img = doc.css('img').first
-
- expect(img.attr('width')).to eq '20'
- expect(img.attr('height')).to eq '20'
+ expect(doc.css('gl-emoji').first.attr('data-unicode-version')).to eq '6.0'
end
it 'keeps whitespace intact' do
doc = filter('This deserves a :+1:, big time.')
- expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
+ expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
it 'unicode keeps whitespace intact' do
doc = filter('This deserves a 🎱, big time.')
- expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
- end
-
- it 'uses a custom asset_root context' do
- root = Gitlab.config.gitlab.url + 'gitlab/root'
-
- doc = filter(':smile:', asset_root: root)
- expect(doc.css('img').first.attr('src')).to start_with(root)
- end
-
- it 'uses a custom asset_host context' do
- ActionController::Base.asset_host = 'https://cdn.example.com'
-
- doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
- expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
- end
-
- it 'uses a custom asset_root context' do
- root = Gitlab.config.gitlab.url + 'gitlab/root'
-
- doc = filter("'🎱'", asset_root: root)
- expect(doc.css('img').first.attr('src')).to start_with(root)
- end
-
- it 'uses a custom asset_host context' do
- ActionController::Base.asset_host = 'https://cdn.example.com'
-
- doc = filter("'🎱'", asset_host: 'https://this-is-ignored-i-guess?')
- expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
+ expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
end
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index b38e3b17e64..b4cd5f63a15 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -86,6 +86,16 @@ describe Banzai::Filter::SanitizationFilter, lib: true do
expect(filter(act).to_html).to eq exp
end
+ it 'allows `summary` elements' do
+ exp = act = '<summary>summary line</summary>'
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'allows `details` elements' do
+ exp = act = '<details>long text goes here</details>'
+ expect(filter(act).to_html).to eq exp
+ end
+
it 'removes `rel` attribute from `a` elements' do
act = %q{<a href="#" rel="nofollow">Link</a>}
exp = %q{<a href="#">Link</a>}
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 7145f0da1d3..53abc056602 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -15,9 +15,9 @@ module Ci
end
describe '#build_attributes' do
- describe 'coverage entry' do
- subject { described_class.new(config, path).build_attributes(:rspec) }
+ subject { described_class.new(config, path).build_attributes(:rspec) }
+ describe 'coverage entry' do
describe 'code coverage regexp' do
let(:config) do
YAML.dump(rspec: { script: 'rspec',
@@ -30,6 +30,56 @@ module Ci
end
end
end
+
+ describe 'allow failure entry' do
+ context 'when job is a manual action' do
+ context 'when allow_failure is defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ when: 'manual',
+ allow_failure: false })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+
+ context 'when allow_failure is not defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ when: 'manual' })
+ end
+
+ it 'is allowed to fail' do
+ expect(subject[:allow_failure]).to be true
+ end
+ end
+ end
+
+ context 'when job is not a manual action' do
+ context 'when allow_failure is defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec',
+ allow_failure: false })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+
+ context 'when allow_failure is not defined' do
+ let(:config) do
+ YAML.dump(rspec: { script: 'rspec' })
+ end
+
+ it 'is not allowed to fail' do
+ expect(subject[:allow_failure]).to be false
+ end
+ end
+ end
+ end
end
describe "#builds_for_ref" do
diff --git a/spec/lib/gitlab/award_emoji_spec.rb b/spec/lib/gitlab/award_emoji_spec.rb
deleted file mode 100644
index 00a110e31f8..00000000000
--- a/spec/lib/gitlab/award_emoji_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::AwardEmoji do
- describe '.urls' do
- after do
- Gitlab::AwardEmoji.instance_variable_set(:@urls, nil)
- end
-
- subject { Gitlab::AwardEmoji.urls }
-
- it { is_expected.to be_an_instance_of(Array) }
- it { is_expected.not_to be_empty }
-
- context 'every Hash in the Array' do
- it 'has the correct keys and values' do
- subject.each do |hash|
- expect(hash[:name]).to be_an_instance_of(String)
- expect(hash[:path]).to be_an_instance_of(String)
- end
- end
- end
-
- context 'handles relative root' do
- it 'includes the full path' do
- allow(Gitlab::Application.config).to receive(:relative_url_root).and_return('/gitlab')
-
- subject.each do |hash|
- expect(hash[:name]).to be_an_instance_of(String)
- expect(hash[:path]).to start_with('/gitlab')
- end
- end
- end
- end
-
- describe '.emoji_by_category' do
- it "only contains known categories" do
- undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys
- expect(undefined_categories).to be_empty
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
index 70a327c5183..2ed120f356a 100644
--- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb
@@ -24,6 +24,20 @@ describe Gitlab::Ci::Config::Entry::Cache do
expect(entry).to be_valid
end
end
+
+ context 'when key is missing' do
+ let(:config) do
+ { untracked: true,
+ paths: ['some/path/'] }
+ end
+
+ describe '#value' do
+ it 'sets key with the default' do
+ expect(entry.value[:key])
+ .to eq(Gitlab::Ci::Config::Entry::Key.default)
+ end
+ end
+ end
end
context 'when entry value is not correct' do
diff --git a/spec/lib/gitlab/ci/config/entry/factory_spec.rb b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
index 3395b3c645b..8dd48e4efae 100644
--- a/spec/lib/gitlab/ci/config/entry/factory_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/factory_spec.rb
@@ -60,13 +60,13 @@ describe Gitlab::Ci::Config::Entry::Factory do
end
context 'when creating entry with nil value' do
- it 'creates an undefined entry' do
+ it 'creates an unspecified entry' do
entry = factory
.value(nil)
.create!
expect(entry)
- .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified
+ .not_to be_specified
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index ebd80ac5e1d..684d01e9056 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -155,6 +155,7 @@ describe Gitlab::Ci::Config::Entry::Global do
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'] },
variables: { VAR: 'value' },
+ ignore: false,
after_script: ['make clean'] },
spinach: { name: :spinach,
before_script: [],
@@ -165,6 +166,7 @@ describe Gitlab::Ci::Config::Entry::Global do
stage: 'test',
cache: { key: 'k', untracked: true, paths: ['public/'] },
variables: {},
+ ignore: false,
after_script: ['make clean'] },
)
end
@@ -186,7 +188,7 @@ describe Gitlab::Ci::Config::Entry::Global do
it 'contains unspecified nodes' do
expect(global.descendants.first)
- .to be_an_instance_of Gitlab::Ci::Config::Entry::Unspecified
+ .not_to be_specified
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index d20f4ec207d..9249bb9c172 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -144,6 +144,7 @@ describe Gitlab::Ci::Config::Entry::Job do
script: %w[rspec],
commands: "ls\npwd\nrspec",
stage: 'test',
+ ignore: false,
after_script: %w[cleanup])
end
end
@@ -159,4 +160,82 @@ describe Gitlab::Ci::Config::Entry::Job do
end
end
end
+
+ describe '#manual_action?' do
+ context 'when job is a manual action' do
+ let(:config) { { script: 'deploy', when: 'manual' } }
+
+ it 'is a manual action' do
+ expect(entry).to be_manual_action
+ end
+ end
+
+ context 'when job is not a manual action' do
+ let(:config) { { script: 'deploy' } }
+
+ it 'is not a manual action' do
+ expect(entry).not_to be_manual_action
+ end
+ end
+ end
+
+ describe '#ignored?' do
+ context 'when job is a manual action' do
+ context 'when it is not specified if job is allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual' }
+ end
+
+ it 'is an ignored job' do
+ expect(entry).to be_ignored
+ end
+ end
+
+ context 'when job is allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual', allow_failure: true }
+ end
+
+ it 'is an ignored job' do
+ expect(entry).to be_ignored
+ end
+ end
+
+ context 'when job is not allowed to fail' do
+ let(:config) do
+ { script: 'deploy', when: 'manual', allow_failure: false }
+ end
+
+ it 'is not an ignored job' do
+ expect(entry).not_to be_ignored
+ end
+ end
+ end
+
+ context 'when job is not a manual action' do
+ context 'when it is not specified if job is allowed to fail' do
+ let(:config) { { script: 'deploy' } }
+
+ it 'is not an ignored job' do
+ expect(entry).not_to be_ignored
+ end
+ end
+
+ context 'when job is allowed to fail' do
+ let(:config) { { script: 'deploy', allow_failure: true } }
+
+ it 'is an ignored job' do
+ expect(entry).to be_ignored
+ end
+ end
+
+ context 'when job is not allowed to fail' do
+ let(:config) { { script: 'deploy', allow_failure: false } }
+
+ it 'is not an ignored job' do
+ expect(entry).not_to be_ignored
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
index aaebf783962..7d104372ac6 100644
--- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb
@@ -62,10 +62,12 @@ describe Gitlab::Ci::Config::Entry::Jobs do
rspec: { name: :rspec,
script: %w[rspec],
commands: 'rspec',
+ ignore: false,
stage: 'test' },
spinach: { name: :spinach,
script: %w[spinach],
commands: 'spinach',
+ ignore: false,
stage: 'test' })
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/key_spec.rb b/spec/lib/gitlab/ci/config/entry/key_spec.rb
index 0dd36fe1f44..5d4de60bc8a 100644
--- a/spec/lib/gitlab/ci/config/entry/key_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/key_spec.rb
@@ -31,4 +31,10 @@ describe Gitlab::Ci::Config::Entry::Key do
end
end
end
+
+ describe '.default' do
+ it 'returns default key' do
+ expect(described_class.default).to eq 'default'
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index 0c40fca0c1a..8b3bd08cf13 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -192,7 +192,7 @@ describe Gitlab::Ci::Status::Build::Factory do
let(:build) { create(:ci_build, :playable) }
it 'matches correct core status' do
- expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual
end
it 'matches correct extended statuses' do
@@ -200,12 +200,13 @@ describe Gitlab::Ci::Status::Build::Factory do
.to eq [Gitlab::Ci::Status::Build::Play]
end
- it 'fabricates a core skipped status' do
+ it 'fabricates a play detailed status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Play
end
it 'fabricates status with correct details' do
expect(status.text).to eq 'manual'
+ expect(status.group).to eq 'manual'
expect(status.icon).to eq 'icon_status_manual'
expect(status.label).to eq 'manual play action'
expect(status).to have_details
@@ -218,7 +219,7 @@ describe Gitlab::Ci::Status::Build::Factory do
let(:build) { create(:ci_build, :playable, :teardown_environment) }
it 'matches correct core status' do
- expect(factory.core_status).to be_a Gitlab::Ci::Status::Skipped
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Manual
end
it 'matches correct extended statuses' do
@@ -226,12 +227,13 @@ describe Gitlab::Ci::Status::Build::Factory do
.to eq [Gitlab::Ci::Status::Build::Stop]
end
- it 'fabricates a core skipped status' do
+ it 'fabricates a stop detailed status' do
expect(status).to be_a Gitlab::Ci::Status::Build::Stop
end
it 'fabricates status with correct details' do
expect(status.text).to eq 'manual'
+ expect(status.group).to eq 'manual'
expect(status.icon).to eq 'icon_status_manual'
expect(status.label).to eq 'manual stop action'
expect(status).to have_details
diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb
index f3e72ea1796..6c97a4fe5ca 100644
--- a/spec/lib/gitlab/ci/status/build/play_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/play_spec.rb
@@ -6,22 +6,10 @@ describe Gitlab::Ci::Status::Build::Play do
subject { described_class.new(status) }
- describe '#text' do
- it { expect(subject.text).to eq 'manual' }
- end
-
describe '#label' do
it { expect(subject.label).to eq 'manual play action' }
end
- describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_manual' }
- end
-
- describe '#group' do
- it { expect(subject.group).to eq 'manual' }
- end
-
describe 'action details' do
let(:user) { create(:user) }
let(:build) { create(:ci_build) }
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
index 41c2b624774..8d021c35a69 100644
--- a/spec/lib/gitlab/ci/status/build/stop_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -8,22 +8,10 @@ describe Gitlab::Ci::Status::Build::Stop do
described_class.new(status)
end
- describe '#text' do
- it { expect(subject.text).to eq 'manual' }
- end
-
describe '#label' do
it { expect(subject.label).to eq 'manual stop action' }
end
- describe '#icon' do
- it { expect(subject.icon).to eq 'icon_status_manual' }
- end
-
- describe '#group' do
- it { expect(subject.group).to eq 'manual' }
- end
-
describe 'action details' do
let(:user) { create(:user) }
let(:build) { create(:ci_build) }
diff --git a/spec/lib/gitlab/ci/status/canceled_spec.rb b/spec/lib/gitlab/ci/status/canceled_spec.rb
index 38412fe2e4f..768f8926f1d 100644
--- a/spec/lib/gitlab/ci/status/canceled_spec.rb
+++ b/spec/lib/gitlab/ci/status/canceled_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Canceled do
end
describe '#text' do
- it { expect(subject.label).to eq 'canceled' }
+ it { expect(subject.text).to eq 'canceled' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/created_spec.rb b/spec/lib/gitlab/ci/status/created_spec.rb
index 6d847484693..e96c13aede3 100644
--- a/spec/lib/gitlab/ci/status/created_spec.rb
+++ b/spec/lib/gitlab/ci/status/created_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Created do
end
describe '#text' do
- it { expect(subject.label).to eq 'created' }
+ it { expect(subject.text).to eq 'created' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/failed_spec.rb b/spec/lib/gitlab/ci/status/failed_spec.rb
index 990d686d22c..e5da0a91159 100644
--- a/spec/lib/gitlab/ci/status/failed_spec.rb
+++ b/spec/lib/gitlab/ci/status/failed_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Failed do
end
describe '#text' do
- it { expect(subject.label).to eq 'failed' }
+ it { expect(subject.text).to eq 'failed' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/manual_spec.rb b/spec/lib/gitlab/ci/status/manual_spec.rb
new file mode 100644
index 00000000000..3fd3727b92d
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/manual_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Manual do
+ subject do
+ described_class.new(double('subject'), double('user'))
+ end
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'manual' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'manual action' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'icon_status_manual' }
+ end
+
+ describe '#group' do
+ it { expect(subject.group).to eq 'manual' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/pending_spec.rb b/spec/lib/gitlab/ci/status/pending_spec.rb
index 7bb6579c317..8d09cf2a05a 100644
--- a/spec/lib/gitlab/ci/status/pending_spec.rb
+++ b/spec/lib/gitlab/ci/status/pending_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Pending do
end
describe '#text' do
- it { expect(subject.label).to eq 'pending' }
+ it { expect(subject.text).to eq 'pending' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/running_spec.rb b/spec/lib/gitlab/ci/status/running_spec.rb
index 852d6c06baf..10d3bf749c1 100644
--- a/spec/lib/gitlab/ci/status/running_spec.rb
+++ b/spec/lib/gitlab/ci/status/running_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Running do
end
describe '#text' do
- it { expect(subject.label).to eq 'running' }
+ it { expect(subject.text).to eq 'running' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/skipped_spec.rb b/spec/lib/gitlab/ci/status/skipped_spec.rb
index e00b356a24b..10db93d3802 100644
--- a/spec/lib/gitlab/ci/status/skipped_spec.rb
+++ b/spec/lib/gitlab/ci/status/skipped_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Skipped do
end
describe '#text' do
- it { expect(subject.label).to eq 'skipped' }
+ it { expect(subject.text).to eq 'skipped' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/ci/status/success_spec.rb b/spec/lib/gitlab/ci/status/success_spec.rb
index 4a89e1faf40..230f24b94a4 100644
--- a/spec/lib/gitlab/ci/status/success_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Status::Success do
end
describe '#text' do
- it { expect(subject.label).to eq 'passed' }
+ it { expect(subject.text).to eq 'passed' }
end
describe '#label' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index f20b6be51e1..1a1280e5198 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -199,6 +199,7 @@ project:
- project_authorizations
- route
- statistics
+- uploads
award_emoji:
- awardable
- user
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index a32c6131030..8e5e8288c49 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -199,4 +199,58 @@ describe Gitlab::Workhorse, lib: true do
end
end
end
+
+ describe '.set_key_and_notify' do
+ let(:key) { 'test-key' }
+ let(:value) { 'test-value' }
+
+ subject { described_class.set_key_and_notify(key, value, overwrite: overwrite) }
+
+ shared_examples 'set and notify' do
+ it 'set and return the same value' do
+ is_expected.to eq(value)
+ end
+
+ it 'set and notify' do
+ expect_any_instance_of(Redis).to receive(:publish)
+ .with(described_class::NOTIFICATION_CHANNEL, "test-key=test-value")
+
+ subject
+ end
+ end
+
+ context 'when we set a new key' do
+ let(:overwrite) { true }
+
+ it_behaves_like 'set and notify'
+ end
+
+ context 'when we set an existing key' do
+ let(:old_value) { 'existing-key' }
+
+ before do
+ described_class.set_key_and_notify(key, old_value, overwrite: true)
+ end
+
+ context 'and overwrite' do
+ let(:overwrite) { true }
+
+ it_behaves_like 'set and notify'
+ end
+
+ context 'and do not overwrite' do
+ let(:overwrite) { false }
+
+ it 'try to set but return the previous value' do
+ is_expected.to eq(old_value)
+ end
+
+ it 'does not notify' do
+ expect_any_instance_of(Redis).not_to receive(:publish)
+
+ subject
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 0b72a2f979b..1060bf3cbf4 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -7,4 +7,6 @@ RSpec.describe Appearance, type: :model do
it { is_expected.to validate_presence_of(:title) }
it { is_expected.to validate_presence_of(:description) }
+
+ it { is_expected.to have_many(:uploads).dependent(:destroy) }
end
diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb
new file mode 100644
index 00000000000..1aab161ec13
--- /dev/null
+++ b/spec/models/chat_team_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe ChatTeam, type: :model do
+ # Associations
+ it { is_expected.to belong_to(:namespace) }
+
+ # Fields
+ it { is_expected.to respond_to(:name) }
+ it { is_expected.to respond_to(:team_id) }
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 5743c555cbe..2db42a94077 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -20,6 +20,30 @@ describe Ci::Build, :models do
it { is_expected.to validate_presence_of :ref }
it { is_expected.to respond_to :trace_html }
+ describe '#actionize' do
+ context 'when build is a created' do
+ before do
+ build.update_column(:status, :created)
+ end
+
+ it 'makes build a manual action' do
+ expect(build.actionize).to be true
+ expect(build.reload).to be_manual
+ end
+ end
+
+ context 'when build is not created' do
+ before do
+ build.update_column(:status, :pending)
+ end
+
+ it 'does not change build status' do
+ expect(build.actionize).to be false
+ expect(build.reload).to be_pending
+ end
+ end
+ end
+
describe '#any_runners_online?' do
subject { build.any_runners_online? }
@@ -587,13 +611,21 @@ describe Ci::Build, :models do
it { is_expected.to be_falsey }
end
- context 'and build.status is failed' do
+ context 'and build status is failed' do
before do
build.status = 'failed'
end
it { is_expected.to be_truthy }
end
+
+ context 'when build is a manual action' do
+ before do
+ build.status = 'manual'
+ end
+
+ it { is_expected.to be_falsey }
+ end
end
end
@@ -682,12 +714,12 @@ describe Ci::Build, :models do
end
end
- describe '#manual?' do
+ describe '#action?' do
before do
build.update(when: value)
end
- subject { build.manual? }
+ subject { build.action? }
context 'when is set to manual' do
let(:value) { 'manual' }
@@ -703,14 +735,50 @@ describe Ci::Build, :models do
end
end
+ describe '#has_commands?' do
+ context 'when build has commands' do
+ let(:build) do
+ create(:ci_build, commands: 'rspec')
+ end
+
+ it 'has commands' do
+ expect(build).to have_commands
+ end
+ end
+
+ context 'when does not have commands' do
+ context 'when commands are an empty string' do
+ let(:build) do
+ create(:ci_build, commands: '')
+ end
+
+ it 'has no commands' do
+ expect(build).not_to have_commands
+ end
+ end
+
+ context 'when commands are not set at all' do
+ let(:build) do
+ create(:ci_build, commands: nil)
+ end
+
+ it 'has no commands' do
+ expect(build).not_to have_commands
+ end
+ end
+ end
+ end
+
describe '#has_tags?' do
context 'when build has tags' do
subject { create(:ci_build, tag_list: ['tag']) }
+
it { is_expected.to have_tags }
end
context 'when build does not have tags' do
subject { create(:ci_build, tag_list: []) }
+
it { is_expected.not_to have_tags }
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index c2fc8c02bb3..dd5f7098d06 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -24,6 +24,14 @@ describe Ci::Pipeline, models: true do
it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha }
+ describe '#block' do
+ it 'changes pipeline status to manual' do
+ expect(pipeline.block).to be true
+ expect(pipeline.reload).to be_manual
+ expect(pipeline.reload).to be_blocked
+ end
+ end
+
describe '#valid_commit_sha' do
context 'commit.sha can not start with 00000000' do
before do
@@ -635,6 +643,14 @@ describe Ci::Pipeline, models: true do
end
end
+ context 'when pipeline is blocked' do
+ let(:pipeline) { create(:ci_pipeline, status: :manual) }
+
+ it 'returns detailed status for blocked pipeline' do
+ expect(subject.text).to eq 'manual'
+ end
+ end
+
context 'when pipeline is successful but with warnings' do
let(:pipeline) { create(:ci_pipeline, status: :success) }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 36533bdd11e..ea5e4e21039 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -158,7 +158,7 @@ describe CommitStatus, :models do
end
end
- describe '.exclude_ignored' do
+ describe '.after_stage' do
subject { described_class.after_stage(0) }
let(:statuses) do
@@ -185,11 +185,32 @@ describe CommitStatus, :models do
create_status(allow_failure: true, status: 'success'),
create_status(allow_failure: true, status: 'failed'),
create_status(allow_failure: false, status: 'success'),
- create_status(allow_failure: false, status: 'failed')]
+ create_status(allow_failure: false, status: 'failed'),
+ create_status(allow_failure: true, status: 'manual'),
+ create_status(allow_failure: false, status: 'manual')]
+ end
+
+ it 'returns statuses without what we want to ignore' do
+ is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9, 11))
+ end
+ end
+
+ describe '.failed_but_allowed' do
+ subject { described_class.failed_but_allowed.order(:id) }
+
+ let(:statuses) do
+ [create_status(allow_failure: true, status: 'success'),
+ create_status(allow_failure: true, status: 'failed'),
+ create_status(allow_failure: false, status: 'success'),
+ create_status(allow_failure: false, status: 'failed'),
+ create_status(allow_failure: true, status: 'canceled'),
+ create_status(allow_failure: false, status: 'canceled'),
+ create_status(allow_failure: true, status: 'manual'),
+ create_status(allow_failure: false, status: 'manual')]
end
it 'returns statuses without what we want to ignore' do
- is_expected.to eq(statuses.values_at(0, 1, 2, 3, 4, 5, 6, 8, 9))
+ is_expected.to eq(statuses.values_at(1, 4))
end
end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index dbfe3cd2d36..f134da441c2 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -109,6 +109,24 @@ describe HasStatus do
it { is_expected.to eq 'running' }
end
+
+ context 'when one status is a blocking manual action' do
+ let!(:statuses) do
+ [create(type, status: :failed),
+ create(type, status: :manual, allow_failure: false)]
+ end
+
+ it { is_expected.to eq 'manual' }
+ end
+
+ context 'when one status is a non-blocking manual action' do
+ let!(:statuses) do
+ [create(type, status: :failed),
+ create(type, status: :manual, allow_failure: true)]
+ end
+
+ it { is_expected.to eq 'failed' }
+ end
end
context 'ci build statuses' do
@@ -218,6 +236,18 @@ describe HasStatus do
it_behaves_like 'not containing the job', status
end
end
+
+ describe '.manual' do
+ subject { CommitStatus.manual }
+
+ %i[manual].each do |status|
+ it_behaves_like 'containing the job', status
+ end
+
+ %i[failed success skipped canceled].each do |status|
+ it_behaves_like 'not containing the job', status
+ end
+ end
end
describe '::DEFAULT_STATUS' do
@@ -225,4 +255,10 @@ describe HasStatus do
expect(described_class::DEFAULT_STATUS).to eq 'created'
end
end
+
+ describe '::BLOCKED_STATUS' do
+ it 'is a status manual' do
+ expect(described_class::BLOCKED_STATUS).to eq 'manual'
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index a4e6eb4e3a6..5d87938235a 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -13,6 +13,8 @@ describe Group, models: true do
it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:labels).class_name('GroupLabel') }
+ it { is_expected.to have_many(:uploads).dependent(:destroy) }
+ it { is_expected.to have_one(:chat_team) }
describe '#members & #requesters' do
let(:requester) { create(:user) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 3f9c4289de9..7525a1b79ee 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -28,6 +28,20 @@ describe Namespace, models: true do
expect(nested).not_to be_valid
expect(nested.errors[:parent_id].first).to eq('has too deep level of nesting')
end
+
+ describe 'reserved path validation' do
+ context 'nested group' do
+ let(:group) { build(:group, :nested, path: 'tree') }
+
+ it { expect(group).not_to be_valid }
+ end
+
+ context 'top-level group' do
+ let(:group) { build(:group, path: 'tree') }
+
+ it { expect(group).to be_valid }
+ end
+ end
end
describe "Respond to" do
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 585c899cdf9..bf7950ef1c9 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -74,8 +74,10 @@ describe KubernetesService, models: true, caching: true do
describe '#initialize_properties' do
context 'with a project' do
- it 'defaults to the project name' do
- expect(described_class.new(project: project).namespace).to eq(project.name)
+ let(:namespace_name) { "#{project.path}-#{project.id}" }
+
+ it 'defaults to the project name with ID' do
+ expect(described_class.new(project: project).namespace).to eq(namespace_name)
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index ee4f4092062..84bdcbe8e59 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -71,6 +71,7 @@ describe Project, models: true do
it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:forks).through(:forked_project_links) }
+ it { is_expected.to have_many(:uploads).dependent(:destroy) }
context 'after initialized' do
it "has a project_feature" do
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
new file mode 100644
index 00000000000..4c832c87d6a
--- /dev/null
+++ b/spec/models/upload_spec.rb
@@ -0,0 +1,151 @@
+require 'rails_helper'
+
+describe Upload, type: :model do
+ describe 'assocations' do
+ it { is_expected.to belong_to(:model) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:size) }
+ it { is_expected.to validate_presence_of(:path) }
+ it { is_expected.to validate_presence_of(:model) }
+ it { is_expected.to validate_presence_of(:uploader) }
+ end
+
+ describe 'callbacks' do
+ context 'for a file above the checksum threshold' do
+ it 'schedules checksum calculation' do
+ stub_const('UploadChecksumWorker', spy)
+
+ upload = described_class.create(
+ path: __FILE__,
+ size: described_class::CHECKSUM_THRESHOLD + 1.kilobyte,
+ model: build_stubbed(:user),
+ uploader: double('ExampleUploader')
+ )
+
+ expect(UploadChecksumWorker)
+ .to have_received(:perform_async).with(upload.id)
+ end
+ end
+
+ context 'for a file at or below the checksum threshold' do
+ it 'calculates checksum immediately before save' do
+ upload = described_class.new(
+ path: __FILE__,
+ size: described_class::CHECKSUM_THRESHOLD,
+ model: build_stubbed(:user),
+ uploader: double('ExampleUploader')
+ )
+
+ expect { upload.save }
+ .to change { upload.checksum }.from(nil)
+ .to(a_string_matching(/\A\h{64}\z/))
+ end
+ end
+ end
+
+ describe '.remove_path' do
+ it 'removes all records at the given path' do
+ described_class.create!(
+ size: File.size(__FILE__),
+ path: __FILE__,
+ model: build_stubbed(:user),
+ uploader: 'AvatarUploader'
+ )
+
+ expect { described_class.remove_path(__FILE__) }.
+ to change { described_class.count }.from(1).to(0)
+ end
+ end
+
+ describe '.record' do
+ let(:fake_uploader) do
+ double(
+ file: double(size: 12_345),
+ relative_path: 'foo/bar.jpg',
+ model: build_stubbed(:user),
+ class: 'AvatarUploader'
+ )
+ end
+
+ it 'removes existing paths before creation' do
+ expect(described_class).to receive(:remove_path)
+ .with(fake_uploader.relative_path)
+
+ described_class.record(fake_uploader)
+ end
+
+ it 'creates a new record and assigns size, path, model, and uploader' do
+ upload = described_class.record(fake_uploader)
+
+ aggregate_failures do
+ expect(upload).to be_persisted
+ expect(upload.size).to eq fake_uploader.file.size
+ expect(upload.path).to eq fake_uploader.relative_path
+ expect(upload.model_id).to eq fake_uploader.model.id
+ expect(upload.model_type).to eq fake_uploader.model.class.to_s
+ expect(upload.uploader).to eq fake_uploader.class
+ end
+ end
+ end
+
+ describe '#absolute_path' do
+ it 'returns the path directly when already absolute' do
+ path = '/path/to/namespace/project/secret/file.jpg'
+ upload = described_class.new(path: path)
+
+ expect(upload).not_to receive(:uploader_class)
+
+ expect(upload.absolute_path).to eq path
+ end
+
+ it "delegates to the uploader's absolute_path method" do
+ uploader = spy('FakeUploader')
+ upload = described_class.new(path: 'secret/file.jpg')
+ expect(upload).to receive(:uploader_class).and_return(uploader)
+
+ upload.absolute_path
+
+ expect(uploader).to have_received(:absolute_path).with(upload)
+ end
+ end
+
+ describe '#calculate_checksum' do
+ it 'calculates the SHA256 sum' do
+ upload = described_class.new(
+ path: __FILE__,
+ size: described_class::CHECKSUM_THRESHOLD - 1.megabyte
+ )
+ expected = Digest::SHA256.file(__FILE__).hexdigest
+
+ expect { upload.calculate_checksum }
+ .to change { upload.checksum }.from(nil).to(expected)
+ end
+
+ it 'returns nil for a non-existant file' do
+ upload = described_class.new(
+ path: __FILE__,
+ size: described_class::CHECKSUM_THRESHOLD - 1.megabyte
+ )
+
+ expect(upload).to receive(:exist?).and_return(false)
+
+ expect(upload.calculate_checksum).to be_nil
+ end
+ end
+
+ describe '#exist?' do
+ it 'returns true when the file exists' do
+ upload = described_class.new(path: __FILE__)
+
+ expect(upload).to exist
+ end
+
+ it 'returns false when the file does not exist' do
+ upload = described_class.new(path: "#{__FILE__}-nope")
+
+ expect(upload).not_to exist
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b99cde64675..adb5b538922 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -36,6 +36,7 @@ describe User, models: true do
it { is_expected.to have_many(:builds).dependent(:nullify) }
it { is_expected.to have_many(:pipelines).dependent(:nullify) }
it { is_expected.to have_many(:chat_names).dependent(:destroy) }
+ it { is_expected.to have_many(:uploads).dependent(:destroy) }
describe '#group_members' do
it 'does not include group memberships for which user is a requester' do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index f2fd1dfc8db..b54ee8e8b85 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -15,6 +15,8 @@ describe API::Environments, api: true do
describe 'GET /projects/:id/environments' do
context 'as member of the project' do
it 'returns project environments' do
+ project_data_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
+
get api("/projects/#{project.id}/environments", user)
expect(response).to have_http_status(200)
@@ -23,8 +25,7 @@ describe API::Environments, api: true do
expect(json_response.size).to eq(1)
expect(json_response.first['name']).to eq(environment.name)
expect(json_response.first['external_url']).to eq(environment.external_url)
- expect(json_response.first['project']['id']).to eq(project.id)
- expect(json_response.first['project']['visibility']).to be_present
+ expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys)
end
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index d31f1bdfb7c..4baccacd448 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -120,7 +120,6 @@ describe 'project routing' do
end
end
- # emojis_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/emojis(.:format) projects/autocomplete_sources#emojis
# members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members
# issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues
# merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests
@@ -128,7 +127,7 @@ describe 'project routing' do
# milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones
# commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands
describe Projects::AutocompleteSourcesController, 'routing' do
- [:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
+ [:members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
it "to ##{action}" do
expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index de68fb64726..d93616c4f50 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::ProcessPipelineService, :services do
+describe Ci::ProcessPipelineService, '#execute', :services do
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
@@ -12,379 +12,518 @@ describe Ci::ProcessPipelineService, :services do
project.add_developer(user)
end
- describe '#execute' do
- context 'start queuing next builds' do
- before do
- create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'rspec', stage_idx: 1)
- create(:ci_build, :created, pipeline: pipeline, name: 'rubocop', stage_idx: 1)
- create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 2)
- end
+ context 'when simple pipeline is defined' do
+ before do
+ create_build('linux', stage_idx: 0)
+ create_build('mac', stage_idx: 0)
+ create_build('rspec', stage_idx: 1)
+ create_build('rubocop', stage_idx: 1)
+ create_build('deploy', stage_idx: 2)
+ end
- it 'processes a pipeline' do
- expect(process_pipeline).to be_truthy
- succeed_pending
- expect(builds.success.count).to eq(2)
+ it 'processes a pipeline' do
+ expect(process_pipeline).to be_truthy
- expect(process_pipeline).to be_truthy
- succeed_pending
- expect(builds.success.count).to eq(4)
+ succeed_pending
+
+ expect(builds.success.count).to eq(2)
+ expect(process_pipeline).to be_truthy
+
+ succeed_pending
+
+ expect(builds.success.count).to eq(4)
+ expect(process_pipeline).to be_truthy
+
+ succeed_pending
+
+ expect(builds.success.count).to eq(5)
+ expect(process_pipeline).to be_falsey
+ end
+
+ it 'does not process pipeline if existing stage is running' do
+ expect(process_pipeline).to be_truthy
+ expect(builds.pending.count).to eq(2)
+
+ expect(process_pipeline).to be_falsey
+ expect(builds.pending.count).to eq(2)
+ end
+ end
+
+ context 'custom stage with first job allowed to fail' do
+ before do
+ create_build('clean_job', stage_idx: 0, allow_failure: true)
+ create_build('test_job', stage_idx: 1, allow_failure: true)
+ end
+ it 'automatically triggers a next stage when build finishes' do
+ expect(process_pipeline).to be_truthy
+ expect(builds_statuses).to eq ['pending']
+
+ fail_running_or_pending
+
+ expect(builds_statuses).to eq %w(failed pending)
+ end
+ end
+
+ context 'when optional manual actions are defined' do
+ before do
+ create_build('build', stage_idx: 0)
+ create_build('test', stage_idx: 1)
+ create_build('test_failure', stage_idx: 2, when: 'on_failure')
+ create_build('deploy', stage_idx: 3)
+ create_build('production', stage_idx: 3, when: 'manual', allow_failure: true)
+ create_build('cleanup', stage_idx: 4, when: 'always')
+ create_build('clear:cache', stage_idx: 4, when: 'manual', allow_failure: true)
+ end
+
+ context 'when builds are successful' do
+ it 'properly processes the pipeline' do
expect(process_pipeline).to be_truthy
- succeed_pending
- expect(builds.success.count).to eq(5)
+ expect(builds_names).to eq ['build']
+ expect(builds_statuses).to eq ['pending']
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test)
+ expect(builds_statuses).to eq %w(success pending)
- expect(process_pipeline).to be_falsey
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test deploy production)
+ expect(builds_statuses).to eq %w(success success pending manual)
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test deploy production cleanup clear:cache)
+ expect(builds_statuses).to eq %w(success success success manual pending manual)
+
+ succeed_running_or_pending
+
+ expect(builds_statuses).to eq %w(success success success manual success manual)
+ expect(pipeline.reload.status).to eq 'success'
end
+ end
- it 'does not process pipeline if existing stage is running' do
+ context 'when test job fails' do
+ it 'properly processes the pipeline' do
expect(process_pipeline).to be_truthy
- expect(builds.pending.count).to eq(2)
+ expect(builds_names).to eq ['build']
+ expect(builds_statuses).to eq ['pending']
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test)
+ expect(builds_statuses).to eq %w(success pending)
- expect(process_pipeline).to be_falsey
- expect(builds.pending.count).to eq(2)
+ fail_running_or_pending
+
+ expect(builds_names).to eq %w(build test test_failure)
+ expect(builds_statuses).to eq %w(success failed pending)
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test test_failure cleanup)
+ expect(builds_statuses).to eq %w(success failed success pending)
+
+ succeed_running_or_pending
+
+ expect(builds_statuses).to eq %w(success failed success success)
+ expect(pipeline.reload.status).to eq 'failed'
end
end
- context 'custom stage with first job allowed to fail' do
- before do
- create(:ci_build, :created, pipeline: pipeline, name: 'clean_job', stage_idx: 0, allow_failure: true)
- create(:ci_build, :created, pipeline: pipeline, name: 'test_job', stage_idx: 1, allow_failure: true)
+ context 'when test and test_failure jobs fail' do
+ it 'properly processes the pipeline' do
+ expect(process_pipeline).to be_truthy
+ expect(builds_names).to eq ['build']
+ expect(builds_statuses).to eq ['pending']
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test)
+ expect(builds_statuses).to eq %w(success pending)
+
+ fail_running_or_pending
+
+ expect(builds_names).to eq %w(build test test_failure)
+ expect(builds_statuses).to eq %w(success failed pending)
+
+ fail_running_or_pending
+
+ expect(builds_names).to eq %w(build test test_failure cleanup)
+ expect(builds_statuses).to eq %w(success failed failed pending)
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test test_failure cleanup)
+ expect(builds_statuses).to eq %w(success failed failed success)
+ expect(pipeline.reload.status).to eq('failed')
end
+ end
- it 'automatically triggers a next stage when build finishes' do
+ context 'when deploy job fails' do
+ it 'properly processes the pipeline' do
expect(process_pipeline).to be_truthy
- expect(builds.pluck(:status)).to contain_exactly('pending')
+ expect(builds_names).to eq ['build']
+ expect(builds_statuses).to eq ['pending']
+
+ succeed_running_or_pending
- pipeline.builds.running_or_pending.each(&:drop)
- expect(builds.pluck(:status)).to contain_exactly('failed', 'pending')
+ expect(builds_names).to eq %w(build test)
+ expect(builds_statuses).to eq %w(success pending)
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w(build test deploy production)
+ expect(builds_statuses).to eq %w(success success pending manual)
+
+ fail_running_or_pending
+
+ expect(builds_names).to eq %w(build test deploy production cleanup)
+ expect(builds_statuses).to eq %w(success success failed manual pending)
+
+ succeed_running_or_pending
+
+ expect(builds_statuses).to eq %w(success success failed manual success)
+ expect(pipeline.reload).to be_failed
end
end
- context 'properly creates builds when "when" is defined' do
- before do
- create(:ci_build, :created, pipeline: pipeline, name: 'build', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'test', stage_idx: 1)
- create(:ci_build, :created, pipeline: pipeline, name: 'test_failure', stage_idx: 2, when: 'on_failure')
- create(:ci_build, :created, pipeline: pipeline, name: 'deploy', stage_idx: 3)
- create(:ci_build, :created, pipeline: pipeline, name: 'production', stage_idx: 3, when: 'manual')
- create(:ci_build, :created, pipeline: pipeline, name: 'cleanup', stage_idx: 4, when: 'always')
- create(:ci_build, :created, pipeline: pipeline, name: 'clear cache', stage_idx: 4, when: 'manual')
- end
+ context 'when build is canceled in the second stage' do
+ it 'does not schedule builds after build has been canceled' do
+ expect(process_pipeline).to be_truthy
+ expect(builds_names).to eq ['build']
+ expect(builds_statuses).to eq ['pending']
- context 'when builds are successful' do
- it 'properly creates builds' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build')
- expect(builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'success', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('success')
- end
- end
+ succeed_running_or_pending
- context 'when test job fails' do
- it 'properly creates builds' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build')
- expect(builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'success', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('failed')
- end
- end
+ expect(builds.running_or_pending).not_to be_empty
+ expect(builds_names).to eq %w(build test)
+ expect(builds_statuses).to eq %w(success pending)
- context 'when test and test_failure jobs fail' do
- it 'properly creates builds' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build')
- expect(builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure')
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'test_failure', 'cleanup')
- expect(builds.pluck(:status)).to contain_exactly('success', 'failed', 'failed', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('failed')
- end
- end
+ cancel_running_or_pending
- context 'when deploy job fails' do
- it 'properly creates builds' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build')
- expect(builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy')
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'pending')
- pipeline.builds.running_or_pending.each(&:drop)
-
- expect(builds.pluck(:name)).to contain_exactly('build', 'test', 'deploy', 'cleanup')
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'pending')
- pipeline.builds.running_or_pending.each(&:success)
-
- expect(builds.pluck(:status)).to contain_exactly('success', 'success', 'failed', 'success')
- pipeline.reload
- expect(pipeline.status).to eq('failed')
- end
+ expect(builds.running_or_pending).to be_empty
+ expect(builds_names).to eq %w[build test]
+ expect(builds_statuses).to eq %w[success canceled]
+ expect(pipeline.reload).to be_canceled
end
+ end
- context 'when build is canceled in the second stage' do
- it 'does not schedule builds after build has been canceled' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build')
- expect(builds.pluck(:status)).to contain_exactly('pending')
- pipeline.builds.running_or_pending.each(&:success)
+ context 'when listing optional manual actions' do
+ it 'returns only for skipped builds' do
+ # currently all builds are created
+ expect(process_pipeline).to be_truthy
+ expect(manual_actions).to be_empty
- expect(builds.running_or_pending).not_to be_empty
+ # succeed stage build
+ succeed_running_or_pending
- expect(builds.pluck(:name)).to contain_exactly('build', 'test')
- expect(builds.pluck(:status)).to contain_exactly('success', 'pending')
- pipeline.builds.running_or_pending.each(&:cancel)
+ expect(manual_actions).to be_empty
- expect(builds.running_or_pending).to be_empty
- expect(pipeline.reload.status).to eq('canceled')
- end
- end
+ # succeed stage test
+ succeed_running_or_pending
+
+ expect(manual_actions).to be_one # production
+
+ # succeed stage deploy
+ succeed_running_or_pending
- context 'when listing manual actions' do
- it 'returns only for skipped builds' do
- # currently all builds are created
- expect(process_pipeline).to be_truthy
- expect(manual_actions).to be_empty
+ expect(manual_actions).to be_many # production and clear cache
+ end
+ end
+ end
- # succeed stage build
- pipeline.builds.running_or_pending.each(&:success)
- expect(manual_actions).to be_empty
+ context 'when there are manual action in earlier stages' do
+ context 'when first stage has only optional manual actions' do
+ before do
+ create_build('build', stage_idx: 0, when: 'manual', allow_failure: true)
+ create_build('check', stage_idx: 1)
+ create_build('test', stage_idx: 2)
- # succeed stage test
- pipeline.builds.running_or_pending.each(&:success)
- expect(manual_actions).to be_one # production
+ process_pipeline
+ end
- # succeed stage deploy
- pipeline.builds.running_or_pending.each(&:success)
- expect(manual_actions).to be_many # production and clear cache
- end
+ it 'starts from the second stage' do
+ expect(all_builds_statuses).to eq %w[manual pending created]
end
end
- context 'when there are manual/on_failure jobs in earlier stages' do
+ context 'when second stage has only optional manual actions' do
before do
- builds
+ create_build('check', stage_idx: 0)
+ create_build('build', stage_idx: 1, when: 'manual', allow_failure: true)
+ create_build('test', stage_idx: 2)
+
process_pipeline
- builds.each(&:reload)
end
- context 'when first stage has only manual jobs' do
- let(:builds) do
- [create_build('build', 0, 'manual'),
- create_build('check', 1),
- create_build('test', 2)]
- end
+ it 'skips second stage and continues on third stage' do
+ expect(all_builds_statuses).to eq(%w[pending created created])
- it 'starts from the second stage' do
- expect(builds.map(&:status)).to eq(%w[skipped pending created])
- end
+ builds.first.success
+
+ expect(all_builds_statuses).to eq(%w[success manual pending])
end
+ end
+ end
+
+ context 'when blocking manual actions are defined' do
+ before do
+ create_build('code:test', stage_idx: 0)
+ create_build('staging:deploy', stage_idx: 1, when: 'manual')
+ create_build('staging:test', stage_idx: 2, when: 'on_success')
+ create_build('production:deploy', stage_idx: 3, when: 'manual')
+ create_build('production:test', stage_idx: 4, when: 'always')
+ end
- context 'when second stage has only manual jobs' do
- let(:builds) do
- [create_build('check', 0),
- create_build('build', 1, 'manual'),
- create_build('test', 2)]
- end
+ context 'when first stage succeeds' do
+ it 'blocks pipeline on stage with first manual action' do
+ process_pipeline
- it 'skips second stage and continues on third stage' do
- expect(builds.map(&:status)).to eq(%w[pending created created])
+ expect(builds_names).to eq %w[code:test]
+ expect(builds_statuses).to eq %w[pending]
+ expect(pipeline.reload.status).to eq 'pending'
- builds.first.success
- builds.each(&:reload)
+ succeed_running_or_pending
- expect(builds.map(&:status)).to eq(%w[success skipped pending])
- end
+ expect(builds_names).to eq %w[code:test staging:deploy]
+ expect(builds_statuses).to eq %w[success manual]
+ expect(pipeline.reload).to be_manual
end
+ end
+
+ context 'when first stage fails' do
+ it 'does not take blocking action into account' do
+ process_pipeline
+
+ expect(builds_names).to eq %w[code:test]
+ expect(builds_statuses).to eq %w[pending]
+ expect(pipeline.reload.status).to eq 'pending'
- context 'when second stage has only on_failure jobs' do
- let(:builds) do
- [create_build('check', 0),
- create_build('build', 1, 'on_failure'),
- create_build('test', 2)]
- end
+ fail_running_or_pending
- it 'skips second stage and continues on third stage' do
- expect(builds.map(&:status)).to eq(%w[pending created created])
+ expect(builds_names).to eq %w[code:test production:test]
+ expect(builds_statuses).to eq %w[failed pending]
- builds.first.success
- builds.each(&:reload)
+ succeed_running_or_pending
- expect(builds.map(&:status)).to eq(%w[success skipped pending])
- end
+ expect(builds_statuses).to eq %w[failed success]
+ expect(pipeline.reload).to be_failed
end
end
- context 'when failed build in the middle stage is retried' do
- context 'when failed build is the only unsuccessful build in the stage' do
- before do
- create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1)
- create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1)
- create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2)
- create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2)
- end
+ context 'when pipeline is promoted sequentially up to the end' do
+ it 'properly processes entire pipeline' do
+ process_pipeline
+
+ expect(builds_names).to eq %w[code:test]
+ expect(builds_statuses).to eq %w[pending]
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w[code:test staging:deploy]
+ expect(builds_statuses).to eq %w[success manual]
+ expect(pipeline.reload).to be_manual
+
+ play_manual_action('staging:deploy')
+
+ expect(builds_statuses).to eq %w[success pending]
+
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w[code:test staging:deploy staging:test]
+ expect(builds_statuses).to eq %w[success success pending]
+
+ succeed_running_or_pending
- it 'does trigger builds in the next stage' do
- expect(process_pipeline).to be_truthy
- expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2')
+ expect(builds_names).to eq %w[code:test staging:deploy staging:test
+ production:deploy]
+ expect(builds_statuses).to eq %w[success success success manual]
- pipeline.builds.running_or_pending.each(&:success)
+ expect(pipeline.reload).to be_manual
+ expect(pipeline.reload).to be_blocked
+ expect(pipeline.reload).not_to be_active
+ expect(pipeline.reload).not_to be_complete
- expect(builds.pluck(:name))
- .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+ play_manual_action('production:deploy')
- pipeline.builds.find_by(name: 'test:1').success
- pipeline.builds.find_by(name: 'test:2').drop
+ expect(builds_statuses).to eq %w[success success success pending]
+ expect(pipeline.reload).to be_running
- expect(builds.pluck(:name))
- .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+ succeed_running_or_pending
- Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success
+ expect(builds_names).to eq %w[code:test staging:deploy staging:test
+ production:deploy production:test]
+ expect(builds_statuses).to eq %w[success success success success pending]
+ expect(pipeline.reload).to be_running
- expect(builds.pluck(:name)).to contain_exactly(
- 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
- end
+ succeed_running_or_pending
+
+ expect(builds_names).to eq %w[code:test staging:deploy staging:test
+ production:deploy production:test]
+ expect(builds_statuses).to eq %w[success success success success success]
+ expect(pipeline.reload).to be_success
end
end
+ end
- context 'when there are builds that are not created yet' do
- let(:pipeline) do
- create(:ci_pipeline, config: config)
- end
+ context 'when second stage has only on_failure jobs' do
+ before do
+ create_build('check', stage_idx: 0)
+ create_build('build', stage_idx: 1, when: 'on_failure')
+ create_build('test', stage_idx: 2)
- let(:config) do
- { rspec: { stage: 'test', script: 'rspec' },
- deploy: { stage: 'deploy', script: 'rsync' } }
- end
+ process_pipeline
+ end
+
+ it 'skips second stage and continues on third stage' do
+ expect(all_builds_statuses).to eq(%w[pending created created])
+
+ builds.first.success
+
+ expect(all_builds_statuses).to eq(%w[success skipped pending])
+ end
+ end
+ context 'when failed build in the middle stage is retried' do
+ context 'when failed build is the only unsuccessful build in the stage' do
before do
- create(:ci_build, :created, pipeline: pipeline, name: 'linux', stage: 'build', stage_idx: 0)
- create(:ci_build, :created, pipeline: pipeline, name: 'mac', stage: 'build', stage_idx: 0)
+ create_build('build:1', stage_idx: 0)
+ create_build('build:2', stage_idx: 0)
+ create_build('test:1', stage_idx: 1)
+ create_build('test:2', stage_idx: 1)
+ create_build('deploy:1', stage_idx: 2)
+ create_build('deploy:2', stage_idx: 2)
end
- it 'processes the pipeline' do
- # Currently we have five builds with state created
- #
- expect(builds.count).to eq(0)
- expect(all_builds.count).to eq(2)
+ it 'does trigger builds in the next stage' do
+ expect(process_pipeline).to be_truthy
+ expect(builds_names).to eq ['build:1', 'build:2']
- # Process builds service will enqueue builds from the first stage.
- #
- process_pipeline
+ succeed_running_or_pending
- expect(builds.count).to eq(2)
- expect(all_builds.count).to eq(2)
+ expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2']
- # When builds succeed we will enqueue remaining builds.
- #
- # We will have 2 succeeded, 1 pending (from stage test), total 4 (two
- # additional build from `.gitlab-ci.yml`).
- #
- succeed_pending
- process_pipeline
+ pipeline.builds.find_by(name: 'test:1').success
+ pipeline.builds.find_by(name: 'test:2').drop
- expect(builds.success.count).to eq(2)
- expect(builds.pending.count).to eq(1)
- expect(all_builds.count).to eq(4)
+ expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2']
- # When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage.
- #
- succeed_pending
- process_pipeline
+ Ci::Build.retry(pipeline.builds.find_by(name: 'test:2'), user).success
- expect(builds.pending.count).to eq(1)
- expect(builds.success.count).to eq(3)
- expect(all_builds.count).to eq(4)
+ expect(builds_names).to eq ['build:1', 'build:2', 'test:1', 'test:2',
+ 'test:2', 'deploy:1', 'deploy:2']
+ end
+ end
+ end
- # When the last one succeeds we have 4 successful builds.
- #
- succeed_pending
- process_pipeline
+ context 'when there are builds that are not created yet' do
+ let(:pipeline) do
+ create(:ci_pipeline, config: config)
+ end
- expect(builds.success.count).to eq(4)
- expect(all_builds.count).to eq(4)
- end
+ let(:config) do
+ { rspec: { stage: 'test', script: 'rspec' },
+ deploy: { stage: 'deploy', script: 'rsync' } }
+ end
+
+ before do
+ create_build('linux', stage: 'build', stage_idx: 0)
+ create_build('mac', stage: 'build', stage_idx: 0)
+ end
+
+ it 'processes the pipeline' do
+ # Currently we have five builds with state created
+ #
+ expect(builds.count).to eq(0)
+ expect(all_builds.count).to eq(2)
+
+ # Process builds service will enqueue builds from the first stage.
+ #
+ process_pipeline
+
+ expect(builds.count).to eq(2)
+ expect(all_builds.count).to eq(2)
+
+ # When builds succeed we will enqueue remaining builds.
+ #
+ # We will have 2 succeeded, 1 pending (from stage test), total 4 (two
+ # additional build from `.gitlab-ci.yml`).
+ #
+ succeed_pending
+ process_pipeline
+
+ expect(builds.success.count).to eq(2)
+ expect(builds.pending.count).to eq(1)
+ expect(all_builds.count).to eq(4)
+
+ # When pending merge_when_pipeline_succeeds in stage test, we enqueue deploy stage.
+ #
+ succeed_pending
+ process_pipeline
+
+ expect(builds.pending.count).to eq(1)
+ expect(builds.success.count).to eq(3)
+ expect(all_builds.count).to eq(4)
+
+ # When the last one succeeds we have 4 successful builds.
+ #
+ succeed_pending
+ process_pipeline
+
+ expect(builds.success.count).to eq(4)
+ expect(all_builds.count).to eq(4)
end
end
+ def process_pipeline
+ described_class.new(pipeline.project, user).execute(pipeline)
+ end
+
def all_builds
- pipeline.builds
+ pipeline.builds.order(:stage_idx, :id)
end
def builds
all_builds.where.not(status: [:created, :skipped])
end
- def process_pipeline
- described_class.new(pipeline.project, user).execute(pipeline)
+ def builds_names
+ builds.pluck(:name)
+ end
+
+ def builds_statuses
+ builds.pluck(:status)
+ end
+
+ def all_builds_statuses
+ all_builds.pluck(:status)
end
def succeed_pending
builds.pending.update_all(status: 'success')
end
+ def succeed_running_or_pending
+ pipeline.builds.running_or_pending.each(&:success)
+ end
+
+ def fail_running_or_pending
+ pipeline.builds.running_or_pending.each(&:drop)
+ end
+
+ def cancel_running_or_pending
+ pipeline.builds.running_or_pending.each(&:cancel)
+ end
+
+ def play_manual_action(name)
+ builds.find_by(name: name).play(user)
+ end
+
delegate :manual_actions, to: :pipeline
- def create_build(name, stage_idx, when_value = nil)
- create(:ci_build,
- :created,
- pipeline: pipeline,
- name: name,
- stage_idx: stage_idx,
- when: when_value)
+ def create_build(name, **opts)
+ create(:ci_build, :created, pipeline: pipeline, name: name, **opts)
end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 8b1ed6470e4..5445b65f4e8 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -89,35 +89,74 @@ describe Ci::RetryPipelineService, '#execute', :services do
end
context 'when pipeline contains manual actions' do
- context 'when there is a canceled manual action in first stage' do
- before do
- create_build('rspec 1', :failed, 0)
- create_build('staging', :canceled, 0, :manual)
- create_build('rspec 2', :canceled, 1)
+ context 'when there are optional manual actions only' do
+ context 'when there is a canceled manual action in first stage' do
+ before do
+ create_build('rspec 1', :failed, 0)
+ create_build('staging', :canceled, 0, when: :manual, allow_failure: true)
+ create_build('rspec 2', :canceled, 1)
+ end
+
+ it 'retries failed builds and marks subsequent for processing' do
+ service.execute(pipeline)
+
+ expect(build('rspec 1')).to be_pending
+ expect(build('staging')).to be_manual
+ expect(build('rspec 2')).to be_created
+ expect(pipeline.reload).to be_running
+ end
end
+ end
- it 'retries builds failed builds and marks subsequent for processing' do
- service.execute(pipeline)
+ context 'when pipeline has blocking manual actions defined' do
+ context 'when pipeline retry should enqueue builds' do
+ before do
+ create_build('test', :failed, 0)
+ create_build('deploy', :canceled, 0, when: :manual, allow_failure: false)
+ create_build('verify', :canceled, 1)
+ end
+
+ it 'retries failed builds' do
+ service.execute(pipeline)
+
+ expect(build('test')).to be_pending
+ expect(build('deploy')).to be_manual
+ expect(build('verify')).to be_created
+ expect(pipeline.reload).to be_running
+ end
+ end
- expect(build('rspec 1')).to be_pending
- expect(build('staging')).to be_skipped
- expect(build('rspec 2')).to be_created
- expect(pipeline.reload).to be_running
+ context 'when pipeline retry should block pipeline immediately' do
+ before do
+ create_build('test', :success, 0)
+ create_build('deploy:1', :success, 1, when: :manual, allow_failure: false)
+ create_build('deploy:2', :failed, 1, when: :manual, allow_failure: false)
+ create_build('verify', :canceled, 2)
+ end
+
+ it 'reprocesses blocking manual action and blocks pipeline' do
+ service.execute(pipeline)
+
+ expect(build('deploy:1')).to be_success
+ expect(build('deploy:2')).to be_manual
+ expect(build('verify')).to be_created
+ expect(pipeline.reload).to be_blocked
+ end
end
end
context 'when there is a skipped manual action in last stage' do
before do
create_build('rspec 1', :canceled, 0)
- create_build('rspec 2', :skipped, 0, :manual)
- create_build('staging', :skipped, 1, :manual)
+ create_build('rspec 2', :skipped, 0, when: :manual, allow_failure: true)
+ create_build('staging', :skipped, 1, when: :manual, allow_failure: true)
end
it 'retries canceled job and reprocesses manual actions' do
service.execute(pipeline)
expect(build('rspec 1')).to be_pending
- expect(build('rspec 2')).to be_skipped
+ expect(build('rspec 2')).to be_manual
expect(build('staging')).to be_created
expect(pipeline.reload).to be_running
end
@@ -126,7 +165,7 @@ describe Ci::RetryPipelineService, '#execute', :services do
context 'when there is a created manual action in the last stage' do
before do
create_build('rspec 1', :canceled, 0)
- create_build('staging', :created, 1, :manual)
+ create_build('staging', :created, 1, when: :manual, allow_failure: true)
end
it 'retries canceled job and does not update the manual action' do
@@ -141,14 +180,14 @@ describe Ci::RetryPipelineService, '#execute', :services do
context 'when there is a created manual action in the first stage' do
before do
create_build('rspec 1', :canceled, 0)
- create_build('staging', :created, 0, :manual)
+ create_build('staging', :created, 0, when: :manual, allow_failure: true)
end
- it 'retries canceled job and skipps the manual action' do
+ it 'retries canceled job and processes the manual action' do
service.execute(pipeline)
expect(build('rspec 1')).to be_pending
- expect(build('staging')).to be_skipped
+ expect(build('staging')).to be_manual
expect(pipeline.reload).to be_running
end
end
@@ -183,13 +222,12 @@ describe Ci::RetryPipelineService, '#execute', :services do
statuses.latest.find_by(name: name)
end
- def create_build(name, status, stage_num, on = 'on_success')
+ def create_build(name, status, stage_num, **opts)
create(:ci_build, name: name,
status: status,
stage: "stage_#{stage_num}",
stage_idx: stage_num,
- when: on,
- pipeline: pipeline) do |build|
+ pipeline: pipeline, **opts) do |build|
pipeline.update_status
end
end
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 14717a7455d..ec89b540e6a 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -4,11 +4,11 @@ describe Groups::CreateService, '#execute', services: true do
let!(:user) { create(:user) }
let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
+ subject { service.execute }
+
describe 'visibility level restrictions' do
let!(:service) { described_class.new(user, group_params) }
- subject { service.execute }
-
context "create groups without restricted visibility level" do
it { is_expected.to be_persisted }
end
@@ -24,8 +24,6 @@ describe Groups::CreateService, '#execute', services: true do
let!(:group) { create(:group) }
let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) }
- subject { service.execute }
-
context 'as group owner' do
before { group.add_owner(user) }
@@ -40,4 +38,20 @@ describe Groups::CreateService, '#execute', services: true do
end
end
end
+
+ describe 'creating a mattermost team' do
+ let!(:params) { group_params.merge(create_chat_team: "true") }
+ let!(:service) { described_class.new(user, params) }
+
+ before do
+ Settings.mattermost['enabled'] = true
+ end
+
+ it 'create the chat team with the group' do
+ allow_any_instance_of(Mattermost::Team).to receive(:create)
+ .and_return({ 'name' => 'tanuki', 'id' => 'lskdjfwlekfjsdifjj' })
+
+ expect { subject }.to change { ChatTeam.count }.from(0).to(1)
+ end
+ end
end
diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb
index c42eeba4b9c..150c8ccaef7 100644
--- a/spec/services/projects/upload_service_spec.rb
+++ b/spec/services/projects/upload_service_spec.rb
@@ -10,7 +10,7 @@ describe Projects::UploadService, services: true do
context 'for valid gif file' do
before do
gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
- @link_to_file = upload_file(@project.repository, gif)
+ @link_to_file = upload_file(@project, gif)
end
it { expect(@link_to_file).to have_key(:alt) }
@@ -23,7 +23,7 @@ describe Projects::UploadService, services: true do
before do
png = fixture_file_upload(Rails.root + 'spec/fixtures/dk.png',
'image/png')
- @link_to_file = upload_file(@project.repository, png)
+ @link_to_file = upload_file(@project, png)
end
it { expect(@link_to_file).to have_key(:alt) }
@@ -35,7 +35,7 @@ describe Projects::UploadService, services: true do
context 'for valid jpg file' do
before do
jpg = fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg')
- @link_to_file = upload_file(@project.repository, jpg)
+ @link_to_file = upload_file(@project, jpg)
end
it { expect(@link_to_file).to have_key(:alt) }
@@ -47,7 +47,7 @@ describe Projects::UploadService, services: true do
context 'for txt file' do
before do
txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
- @link_to_file = upload_file(@project.repository, txt)
+ @link_to_file = upload_file(@project, txt)
end
it { expect(@link_to_file).to have_key(:alt) }
@@ -60,14 +60,14 @@ describe Projects::UploadService, services: true do
before do
txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
allow(txt).to receive(:size) { 1000.megabytes.to_i }
- @link_to_file = upload_file(@project.repository, txt)
+ @link_to_file = upload_file(@project, txt)
end
it { expect(@link_to_file).to eq(nil) }
end
end
- def upload_file(repository, file)
- Projects::UploadService.new(repository, file).execute
+ def upload_file(project, file)
+ Projects::UploadService.new(project, file).execute
end
end
diff --git a/spec/support/carrierwave.rb b/spec/support/carrierwave.rb
index 72af2c70324..b4b016e408f 100644
--- a/spec/support/carrierwave.rb
+++ b/spec/support/carrierwave.rb
@@ -1,7 +1,7 @@
-CarrierWave.root = 'tmp/tests/uploads'
+CarrierWave.root = File.expand_path('tmp/tests/public', Rails.root)
RSpec.configure do |config|
config.after(:each) do
- FileUtils.rm_rf('tmp/tests/uploads')
+ FileUtils.rm_rf(CarrierWave.root)
end
end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 97b8b342eb2..bbbbaf4c5e8 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -26,10 +26,11 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('img.emoji', count: 10)
+ expect(actual).to have_selector('gl-emoji', count: 10)
- image = actual.at_css('img.emoji')
- expect(image['src'].to_s).to start_with(Gitlab.config.gitlab.url + '/assets')
+ emoji_element = actual.at_css('gl-emoji')
+ expect(emoji_element['data-name'].to_s).not_to be_empty
+ expect(emoji_element['data-unicode-version'].to_s).not_to be_empty
end
end
diff --git a/spec/tasks/gitlab/info_rake_spec.rb b/spec/tasks/gitlab/info_rake_spec.rb
new file mode 100644
index 00000000000..ca74378a12a
--- /dev/null
+++ b/spec/tasks/gitlab/info_rake_spec.rb
@@ -0,0 +1,37 @@
+require 'rake_helper'
+
+describe 'gitlab:env:info' do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/info'
+
+ stub_warn_user_is_not_gitlab
+ allow(Gitlab::Popen).to receive(:popen)
+ end
+
+ describe 'git version' do
+ before do
+ allow(Gitlab::Popen).to receive(:popen).with([Gitlab.config.git.bin_path, '--version'])
+ .and_return(git_version)
+ end
+
+ context 'when git installed' do
+ let(:git_version) { 'git version 2.10.0' }
+
+ it 'prints git version' do
+ run_rake_task('gitlab:env:info')
+
+ expect($stdout.string).to match(/Git Version:(.*)2.10.0/)
+ end
+ end
+
+ context 'when git not installed' do
+ let(:git_version) { '' }
+
+ it 'prints unknown' do
+ run_rake_task('gitlab:env:info')
+
+ expect($stdout.string).to match(/Git Version:(.*)unknown/)
+ end
+ end
+ end
+end
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index b0f5be55c33..d9113ef4095 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -1,7 +1,19 @@
require 'spec_helper'
describe FileUploader do
- let(:uploader) { described_class.new(build_stubbed(:project)) }
+ let(:uploader) { described_class.new(build_stubbed(:empty_project)) }
+
+ describe '.absolute_path' do
+ it 'returns the correct absolute path by building it dynamically' do
+ project = build_stubbed(:project)
+ upload = double(model: project, path: 'secret/foo.jpg')
+
+ dynamic_segment = project.path_with_namespace
+
+ expect(described_class.absolute_path(upload))
+ .to end_with("#{dynamic_segment}/secret/foo.jpg")
+ end
+ end
describe 'initialize' do
it 'generates a secret if none is provided' do
@@ -32,4 +44,13 @@ describe FileUploader do
expect(uploader.move_to_store).to eq(true)
end
end
+
+ describe '#relative_path' do
+ it 'removes the leading dynamic path segment' do
+ fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
+ uploader.store!(fixture_file_upload(fixture))
+
+ expect(uploader.relative_path).to match(/\A\h{32}\/rails_sample.jpg\z/)
+ end
+ end
end
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
new file mode 100644
index 00000000000..5c26e334a6e
--- /dev/null
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -0,0 +1,97 @@
+require 'rails_helper'
+
+describe RecordsUploads do
+ let(:uploader) do
+ class RecordsUploadsExampleUploader < GitlabUploader
+ include RecordsUploads
+
+ storage :file
+
+ def model
+ FactoryGirl.build_stubbed(:user)
+ end
+ end
+
+ RecordsUploadsExampleUploader.new
+ end
+
+ def upload_fixture(filename)
+ fixture_file_upload(Rails.root.join('spec', 'fixtures', filename))
+ end
+
+ describe 'callbacks' do
+ it 'calls `record_upload` after `store`' do
+ expect(uploader).to receive(:record_upload).once
+
+ uploader.store!(upload_fixture('doc_sample.txt'))
+ end
+
+ it 'calls `destroy_upload` after `remove`' do
+ expect(uploader).to receive(:destroy_upload).once
+
+ uploader.store!(upload_fixture('doc_sample.txt'))
+
+ uploader.remove!
+ end
+ end
+
+ describe '#record_upload callback' do
+ it 'returns early when not using file storage' do
+ allow(uploader).to receive(:file_storage?).and_return(false)
+ expect(Upload).not_to receive(:record)
+
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+ end
+
+ it "returns early when the file doesn't exist" do
+ allow(uploader).to receive(:file).and_return(double(exists?: false))
+ expect(Upload).not_to receive(:record)
+
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+ end
+
+ it 'creates an Upload record after store' do
+ expect(Upload).to receive(:record)
+ .with(uploader)
+
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+ end
+
+ it 'it destroys Upload records at the same path before recording' do
+ existing = Upload.create!(
+ path: File.join('uploads', 'rails_sample.jpg'),
+ size: 512.kilobytes,
+ model: build_stubbed(:user),
+ uploader: uploader.class.to_s
+ )
+
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+
+ expect { existing.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ expect(Upload.count).to eq 1
+ end
+ end
+
+ describe '#destroy_upload callback' do
+ it 'returns early when not using file storage' do
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+
+ allow(uploader).to receive(:file_storage?).and_return(false)
+ expect(Upload).not_to receive(:remove_path)
+
+ uploader.remove!
+ end
+
+ it 'returns early when file is nil' do
+ expect(Upload).not_to receive(:remove_path)
+
+ uploader.remove!
+ end
+
+ it 'it destroys Upload records at the same path after removal' do
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+
+ expect { uploader.remove! }.to change { Upload.count }.from(1).to(0)
+ end
+ end
+end
diff --git a/spec/uploaders/uploader_helper_spec.rb b/spec/uploaders/uploader_helper_spec.rb
index e9efd13b9aa..c47f09adb6d 100644
--- a/spec/uploaders/uploader_helper_spec.rb
+++ b/spec/uploaders/uploader_helper_spec.rb
@@ -1,10 +1,14 @@
require 'rails_helper'
describe UploaderHelper do
- class ExampleUploader < CarrierWave::Uploader::Base
- include UploaderHelper
+ let(:uploader) do
+ example_uploader = Class.new(CarrierWave::Uploader::Base) do
+ include UploaderHelper
- storage :file
+ storage :file
+ end
+
+ example_uploader.new
end
def upload_fixture(filename)
@@ -12,8 +16,6 @@ describe UploaderHelper do
end
describe '#image_or_video?' do
- let(:uploader) { ExampleUploader.new }
-
it 'returns true for an image file' do
uploader.store!(upload_fixture('dk.png'))
diff --git a/spec/workers/stuck_ci_builds_worker_spec.rb b/spec/workers/stuck_ci_builds_worker_spec.rb
deleted file mode 100644
index 801fa31b45d..00000000000
--- a/spec/workers/stuck_ci_builds_worker_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-require "spec_helper"
-
-describe StuckCiBuildsWorker do
- let!(:build) { create :ci_build }
- let(:worker) { described_class.new }
-
- subject do
- build.reload
- build.status
- end
-
- %w(pending running).each do |status|
- context "#{status} build" do
- before do
- build.update!(status: status)
- end
-
- it 'gets dropped if it was updated over 2 days ago' do
- build.update!(updated_at: 2.days.ago)
- worker.perform
- is_expected.to eq('failed')
- end
-
- it "is still #{status}" do
- build.update!(updated_at: 1.minute.ago)
- worker.perform
- is_expected.to eq(status)
- end
- end
- end
-
- %w(success failed canceled).each do |status|
- context "#{status} build" do
- before do
- build.update!(status: status)
- end
-
- it "is still #{status}" do
- build.update!(updated_at: 2.days.ago)
- worker.perform
- is_expected.to eq(status)
- end
- end
- end
-
- context "for deleted project" do
- before do
- build.update!(status: :running, updated_at: 2.days.ago)
- build.project.update(pending_delete: true)
- end
-
- it "does not drop build" do
- expect_any_instance_of(Ci::Build).not_to receive(:drop)
- worker.perform
- end
- end
-end
diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
new file mode 100644
index 00000000000..8434b0c8e5b
--- /dev/null
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -0,0 +1,129 @@
+require 'spec_helper'
+
+describe StuckCiJobsWorker do
+ let!(:runner) { create :ci_runner }
+ let!(:job) { create :ci_build, runner: runner }
+ let(:worker) { described_class.new }
+ let(:exclusive_lease_uuid) { SecureRandom.uuid }
+
+ subject do
+ job.reload
+ job.status
+ end
+
+ before do
+ job.update!(status: status, updated_at: updated_at)
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
+ end
+
+ shared_examples 'job is dropped' do
+ it 'changes status' do
+ worker.perform
+ is_expected.to eq('failed')
+ end
+ end
+
+ shared_examples 'job is unchanged' do
+ it "doesn't change status" do
+ worker.perform
+ is_expected.to eq(status)
+ end
+ end
+
+ context 'when job is pending' do
+ let(:status) { 'pending' }
+
+ context 'when job is not stuck' do
+ before { allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(false) }
+
+ context 'when job was not updated for more than 1 day ago' do
+ let(:updated_at) { 2.days.ago }
+ it_behaves_like 'job is dropped'
+ end
+
+ context 'when job was updated in less than 1 day ago' do
+ let(:updated_at) { 6.hours.ago }
+ it_behaves_like 'job is unchanged'
+ end
+
+ context 'when job was not updated for more than 1 hour ago' do
+ let(:updated_at) { 2.hours.ago }
+ it_behaves_like 'job is unchanged'
+ end
+ end
+
+ context 'when job is stuck' do
+ before { allow_any_instance_of(Ci::Build).to receive(:stuck?).and_return(true) }
+
+ context 'when job was not updated for more than 1 hour ago' do
+ let(:updated_at) { 2.hours.ago }
+ it_behaves_like 'job is dropped'
+ end
+
+ context 'when job was updated in less than 1 hour ago' do
+ let(:updated_at) { 30.minutes.ago }
+ it_behaves_like 'job is unchanged'
+ end
+ end
+ end
+
+ context 'when job is running' do
+ let(:status) { 'running' }
+
+ context 'when job was not updated for more than 1 hour ago' do
+ let(:updated_at) { 2.hours.ago }
+ it_behaves_like 'job is dropped'
+ end
+
+ context 'when job was updated in less than 1 hour ago' do
+ let(:updated_at) { 30.minutes.ago }
+ it_behaves_like 'job is unchanged'
+ end
+ end
+
+ %w(success skipped failed canceled).each do |status|
+ context "when job is #{status}" do
+ let(:status) { status }
+ let(:updated_at) { 2.days.ago }
+ it_behaves_like 'job is unchanged'
+ end
+ end
+
+ context 'for deleted project' do
+ let(:status) { 'running' }
+ let(:updated_at) { 2.days.ago }
+
+ before { job.project.update(pending_delete: true) }
+
+ it 'does not drop job' do
+ expect_any_instance_of(Ci::Build).not_to receive(:drop)
+ worker.perform
+ end
+ end
+
+ describe 'exclusive lease' do
+ let(:status) { 'running' }
+ let(:updated_at) { 2.days.ago }
+ let(:worker2) { described_class.new }
+
+ it 'is guard by exclusive lease when executed concurrently' do
+ expect(worker).to receive(:drop).at_least(:once)
+ expect(worker2).not_to receive(:drop)
+ worker.perform
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(false)
+ worker2.perform
+ end
+
+ it 'can be executed in sequence' do
+ expect(worker).to receive(:drop).at_least(:once)
+ expect(worker2).to receive(:drop).at_least(:once)
+ worker.perform
+ worker2.perform
+ end
+
+ it 'cancels exclusive lease after worker perform' do
+ expect(Gitlab::ExclusiveLease).to receive(:cancel).with(described_class::EXCLUSIVE_LEASE_KEY, exclusive_lease_uuid)
+ worker.perform
+ end
+ end
+end
diff --git a/spec/workers/upload_checksum_worker_spec.rb b/spec/workers/upload_checksum_worker_spec.rb
new file mode 100644
index 00000000000..911360da66c
--- /dev/null
+++ b/spec/workers/upload_checksum_worker_spec.rb
@@ -0,0 +1,19 @@
+require 'rails_helper'
+
+describe UploadChecksumWorker do
+ describe '#perform' do
+ it 'rescues ActiveRecord::RecordNotFound' do
+ expect { described_class.new.perform(999_999) }.not_to raise_error
+ end
+
+ it 'calls calculate_checksum_without_delay and save!' do
+ upload = spy
+ expect(Upload).to receive(:find).with(999_999).and_return(upload)
+
+ described_class.new.perform(999_999)
+
+ expect(upload).to have_received(:calculate_checksum)
+ expect(upload).to have_received(:save!)
+ end
+ end
+end