From 8b61452138ecc511b52cd49be4ee6b8a80390c50 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Fri, 20 Dec 2019 15:07:34 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- VERSION | 2 +- .../chat_message/wiki_page_message.rb | 2 +- .../error_tracking/detailed_error_entity.rb | 1 + .../119031-add-tags-to-sentry-error-api-rest.yml | 5 + .../39140-scope-modsec-feature-flag-to-groups.yml | 5 + changelogs/unreleased/wiki-page-message.yml | 5 + danger/commit_messages/Dangerfile | 14 +- ...d_modsecurity_enabled_to_ingress_application.rb | 16 + db/schema.rb | 1 + doc/development/fe_guide/design_patterns.md | 6 +- lib/gitlab/error_tracking/detailed_error.rb | 1 + lib/sentry/client/issue.rb | 8 + spec/factories/error_tracking/detailed_error.rb | 6 + .../api/schemas/error_tracking/error_detailed.json | 34 +- spec/frontend/behaviors/bind_in_out_spec.js | 204 ++++++ spec/frontend/bootstrap_jquery_spec.js | 44 ++ .../branches/branches_delete_modal_spec.js | 40 + spec/frontend/breakpoints_spec.js | 27 + .../diffs/components/settings_dropdown_spec.js | 167 +++++ spec/frontend/droplab/constants_spec.js | 39 + spec/frontend/droplab/plugins/ajax_filter_spec.js | 72 ++ spec/frontend/droplab/plugins/ajax_spec.js | 41 ++ .../feature_highlight_options_spec.js | 30 + .../recent_searches_dropdown_content_spec.js | 201 +++++ .../frontend/filtered_search/dropdown_user_spec.js | 113 +++ .../components/frequent_items_search_input_spec.js | 83 +++ spec/frontend/gl_field_errors_spec.js | 144 ++++ spec/frontend/gpg_badges_spec.js | 92 +++ spec/frontend/header_spec.js | 53 ++ spec/frontend/helpers/class_spec_helper_spec.js | 26 + .../components/commit_sidebar/stage_button_spec.js | 46 ++ .../commit_sidebar/unstage_button_spec.js | 39 + .../components/jobs/detail/scroll_button_spec.js | 59 ++ spec/frontend/ide/stores/actions/file_spec.js | 805 +++++++++++++++++++++ .../image_diff/helpers/init_image_diff_spec.js | 52 ++ .../image_diff/init_discussion_tab_spec.js | 42 ++ .../issue_show/components/edit_actions_spec.js | 134 ++++ .../components/fields/description_spec.js | 70 ++ .../issue_show/components/fields/title_spec.js | 48 ++ spec/frontend/issue_show/index_spec.js | 19 + .../jobs/components/job_log_controllers_spec.js | 208 ++++++ spec/frontend/namespace_select_spec.js | 66 ++ spec/frontend/new_branch_spec.js | 203 ++++++ .../components/discussion_filter_note_spec.js | 93 +++ spec/frontend/notes/components/note_header_spec.js | 125 ++++ spec/frontend/notes/stores/getters_spec.js | 388 ++++++++++ spec/frontend/notes/stores/mutation_spec.js | 584 +++++++++++++++ .../shared/components/timezone_dropdown_spec.js | 244 +++++++ spec/frontend/pipelines/nav_controls_spec.js | 85 +++ spec/frontend/polyfills/element_spec.js | 46 ++ .../profile/add_ssh_key_validation_spec.js | 71 ++ spec/frontend/project_select_combo_button_spec.js | 140 ++++ spec/frontend/shared/popover_spec.js | 166 +++++ spec/frontend/sidebar/sidebar_store_spec.js | 168 +++++ spec/frontend/syntax_highlight_spec.js | 48 ++ spec/frontend/task_list_spec.js | 156 ++++ spec/frontend/version_check_image_spec.js | 42 ++ .../vue_shared/components/gl_modal_vuex_spec.js | 151 ++++ .../components/markdown/suggestion_diff_spec.js | 101 +++ .../user_avatar/user_avatar_list_spec.js | 156 ++++ spec/javascripts/behaviors/bind_in_out_spec.js | 192 ----- spec/javascripts/bootstrap_jquery_spec.js | 41 -- .../branches/branches_delete_modal_spec.js | 40 - spec/javascripts/breakpoints_spec.js | 27 - .../diffs/components/settings_dropdown_spec.js | 167 ----- spec/javascripts/droplab/constants_spec.js | 39 - .../droplab/plugins/ajax_filter_spec.js | 72 -- spec/javascripts/droplab/plugins/ajax_spec.js | 41 -- .../feature_highlight_options_spec.js | 30 - .../recent_searches_dropdown_content_spec.js | 201 ----- .../filtered_search/dropdown_user_spec.js | 113 --- .../components/frequent_items_search_input_spec.js | 83 --- spec/javascripts/gl_field_errors_spec.js | 138 ---- spec/javascripts/gpg_badges_spec.js | 92 --- spec/javascripts/header_spec.js | 53 -- spec/javascripts/helpers/class_spec_helper_spec.js | 20 - .../components/commit_sidebar/stage_button_spec.js | 46 -- .../commit_sidebar/unstage_button_spec.js | 39 - .../components/jobs/detail/scroll_button_spec.js | 59 -- spec/javascripts/ide/stores/actions/file_spec.js | 805 --------------------- .../image_diff/helpers/init_image_diff_spec.js | 52 -- .../image_diff/init_discussion_tab_spec.js | 40 - .../issue_show/components/edit_actions_spec.js | 134 ---- .../components/fields/description_spec.js | 70 -- .../issue_show/components/fields/title_spec.js | 48 -- spec/javascripts/issue_show/index_spec.js | 19 - .../jobs/components/job_log_controllers_spec.js | 208 ------ spec/javascripts/namespace_select_spec.js | 66 -- spec/javascripts/new_branch_spec.js | 199 ----- .../components/discussion_filter_note_spec.js | 93 --- .../notes/components/note_header_spec.js | 125 ---- spec/javascripts/notes/stores/getters_spec.js | 388 ---------- spec/javascripts/notes/stores/mutation_spec.js | 584 --------------- .../shared/components/timezone_dropdown_spec.js | 244 ------- spec/javascripts/pipelines/nav_controls_spec.js | 85 --- spec/javascripts/polyfills/element_spec.js | 36 - .../profile/add_ssh_key_validation_spec.js | 69 -- .../project_select_combo_button_spec.js | 124 ---- spec/javascripts/shared/popover_spec.js | 166 ----- spec/javascripts/sidebar/sidebar_store_spec.js | 162 ----- spec/javascripts/syntax_highlight_spec.js | 50 -- spec/javascripts/task_list_spec.js | 156 ---- spec/javascripts/version_check_image_spec.js | 35 - .../vue_shared/components/gl_modal_vuex_spec.js | 151 ---- .../components/markdown/suggestion_diff_spec.js | 101 --- .../user_avatar/user_avatar_list_spec.js | 156 ---- spec/lib/sentry/client/issue_spec.rb | 4 + .../chat_message/wiki_page_message_spec.rb | 19 +- 108 files changed, 6027 insertions(+), 5892 deletions(-) create mode 100644 changelogs/unreleased/119031-add-tags-to-sentry-error-api-rest.yml create mode 100644 changelogs/unreleased/39140-scope-modsec-feature-flag-to-groups.yml create mode 100644 changelogs/unreleased/wiki-page-message.yml create mode 100644 db/migrate/20191217212348_add_modsecurity_enabled_to_ingress_application.rb create mode 100644 spec/frontend/behaviors/bind_in_out_spec.js create mode 100644 spec/frontend/bootstrap_jquery_spec.js create mode 100644 spec/frontend/branches/branches_delete_modal_spec.js create mode 100644 spec/frontend/breakpoints_spec.js create mode 100644 spec/frontend/diffs/components/settings_dropdown_spec.js create mode 100644 spec/frontend/droplab/constants_spec.js create mode 100644 spec/frontend/droplab/plugins/ajax_filter_spec.js create mode 100644 spec/frontend/droplab/plugins/ajax_spec.js create mode 100644 spec/frontend/feature_highlight/feature_highlight_options_spec.js create mode 100644 spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js create mode 100644 spec/frontend/filtered_search/dropdown_user_spec.js create mode 100644 spec/frontend/frequent_items/components/frequent_items_search_input_spec.js create mode 100644 spec/frontend/gl_field_errors_spec.js create mode 100644 spec/frontend/gpg_badges_spec.js create mode 100644 spec/frontend/header_spec.js create mode 100644 spec/frontend/helpers/class_spec_helper_spec.js create mode 100644 spec/frontend/ide/components/commit_sidebar/stage_button_spec.js create mode 100644 spec/frontend/ide/components/commit_sidebar/unstage_button_spec.js create mode 100644 spec/frontend/ide/components/jobs/detail/scroll_button_spec.js create mode 100644 spec/frontend/ide/stores/actions/file_spec.js create mode 100644 spec/frontend/image_diff/helpers/init_image_diff_spec.js create mode 100644 spec/frontend/image_diff/init_discussion_tab_spec.js create mode 100644 spec/frontend/issue_show/components/edit_actions_spec.js create mode 100644 spec/frontend/issue_show/components/fields/description_spec.js create mode 100644 spec/frontend/issue_show/components/fields/title_spec.js create mode 100644 spec/frontend/issue_show/index_spec.js create mode 100644 spec/frontend/jobs/components/job_log_controllers_spec.js create mode 100644 spec/frontend/namespace_select_spec.js create mode 100644 spec/frontend/new_branch_spec.js create mode 100644 spec/frontend/notes/components/discussion_filter_note_spec.js create mode 100644 spec/frontend/notes/components/note_header_spec.js create mode 100644 spec/frontend/notes/stores/getters_spec.js create mode 100644 spec/frontend/notes/stores/mutation_spec.js create mode 100644 spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js create mode 100644 spec/frontend/pipelines/nav_controls_spec.js create mode 100644 spec/frontend/polyfills/element_spec.js create mode 100644 spec/frontend/profile/add_ssh_key_validation_spec.js create mode 100644 spec/frontend/project_select_combo_button_spec.js create mode 100644 spec/frontend/shared/popover_spec.js create mode 100644 spec/frontend/sidebar/sidebar_store_spec.js create mode 100644 spec/frontend/syntax_highlight_spec.js create mode 100644 spec/frontend/task_list_spec.js create mode 100644 spec/frontend/version_check_image_spec.js create mode 100644 spec/frontend/vue_shared/components/gl_modal_vuex_spec.js create mode 100644 spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js create mode 100644 spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js delete mode 100644 spec/javascripts/behaviors/bind_in_out_spec.js delete mode 100644 spec/javascripts/bootstrap_jquery_spec.js delete mode 100644 spec/javascripts/branches/branches_delete_modal_spec.js delete mode 100644 spec/javascripts/breakpoints_spec.js delete mode 100644 spec/javascripts/diffs/components/settings_dropdown_spec.js delete mode 100644 spec/javascripts/droplab/constants_spec.js delete mode 100644 spec/javascripts/droplab/plugins/ajax_filter_spec.js delete mode 100644 spec/javascripts/droplab/plugins/ajax_spec.js delete mode 100644 spec/javascripts/feature_highlight/feature_highlight_options_spec.js delete mode 100644 spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js delete mode 100644 spec/javascripts/filtered_search/dropdown_user_spec.js delete mode 100644 spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js delete mode 100644 spec/javascripts/gl_field_errors_spec.js delete mode 100644 spec/javascripts/gpg_badges_spec.js delete mode 100644 spec/javascripts/header_spec.js delete mode 100644 spec/javascripts/helpers/class_spec_helper_spec.js delete mode 100644 spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js delete mode 100644 spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js delete mode 100644 spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js delete mode 100644 spec/javascripts/ide/stores/actions/file_spec.js delete mode 100644 spec/javascripts/image_diff/helpers/init_image_diff_spec.js delete mode 100644 spec/javascripts/image_diff/init_discussion_tab_spec.js delete mode 100644 spec/javascripts/issue_show/components/edit_actions_spec.js delete mode 100644 spec/javascripts/issue_show/components/fields/description_spec.js delete mode 100644 spec/javascripts/issue_show/components/fields/title_spec.js delete mode 100644 spec/javascripts/issue_show/index_spec.js delete mode 100644 spec/javascripts/jobs/components/job_log_controllers_spec.js delete mode 100644 spec/javascripts/namespace_select_spec.js delete mode 100644 spec/javascripts/new_branch_spec.js delete mode 100644 spec/javascripts/notes/components/discussion_filter_note_spec.js delete mode 100644 spec/javascripts/notes/components/note_header_spec.js delete mode 100644 spec/javascripts/notes/stores/getters_spec.js delete mode 100644 spec/javascripts/notes/stores/mutation_spec.js delete mode 100644 spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js delete mode 100644 spec/javascripts/pipelines/nav_controls_spec.js delete mode 100644 spec/javascripts/polyfills/element_spec.js delete mode 100644 spec/javascripts/profile/add_ssh_key_validation_spec.js delete mode 100644 spec/javascripts/project_select_combo_button_spec.js delete mode 100644 spec/javascripts/shared/popover_spec.js delete mode 100644 spec/javascripts/sidebar/sidebar_store_spec.js delete mode 100644 spec/javascripts/syntax_highlight_spec.js delete mode 100644 spec/javascripts/task_list_spec.js delete mode 100644 spec/javascripts/version_check_image_spec.js delete mode 100644 spec/javascripts/vue_shared/components/gl_modal_vuex_spec.js delete mode 100644 spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js delete mode 100644 spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js diff --git a/VERSION b/VERSION index 56219811eaa..158cd7e61ba 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -12.6.0-pre +12.7.0-pre diff --git a/app/models/project_services/chat_message/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb index b605d289278..ebe7abb379f 100644 --- a/app/models/project_services/chat_message/wiki_page_message.rb +++ b/app/models/project_services/chat_message/wiki_page_message.rb @@ -14,7 +14,7 @@ module ChatMessage obj_attr = HashWithIndifferentAccess.new(obj_attr) @title = obj_attr[:title] @wiki_page_url = obj_attr[:url] - @description = obj_attr[:content] + @description = obj_attr[:message] @action = case obj_attr[:action] diff --git a/app/serializers/error_tracking/detailed_error_entity.rb b/app/serializers/error_tracking/detailed_error_entity.rb index dd0cac8e4cd..fdafa2cf203 100644 --- a/app/serializers/error_tracking/detailed_error_entity.rb +++ b/app/serializers/error_tracking/detailed_error_entity.rb @@ -21,6 +21,7 @@ module ErrorTracking :project_slug, :short_id, :status, + :tags, :title, :type, :user_count diff --git a/changelogs/unreleased/119031-add-tags-to-sentry-error-api-rest.yml b/changelogs/unreleased/119031-add-tags-to-sentry-error-api-rest.yml new file mode 100644 index 00000000000..19713a6fea2 --- /dev/null +++ b/changelogs/unreleased/119031-add-tags-to-sentry-error-api-rest.yml @@ -0,0 +1,5 @@ +--- +title: Add tags to sentry detailed error response +merge_request: 22068 +author: +type: added diff --git a/changelogs/unreleased/39140-scope-modsec-feature-flag-to-groups.yml b/changelogs/unreleased/39140-scope-modsec-feature-flag-to-groups.yml new file mode 100644 index 00000000000..54b0a8ac6db --- /dev/null +++ b/changelogs/unreleased/39140-scope-modsec-feature-flag-to-groups.yml @@ -0,0 +1,5 @@ +--- +title: Add modsecurity_enabled setting to managed ingress +merge_request: 21968 +author: +type: added diff --git a/changelogs/unreleased/wiki-page-message.yml b/changelogs/unreleased/wiki-page-message.yml new file mode 100644 index 00000000000..028c3cfd1e0 --- /dev/null +++ b/changelogs/unreleased/wiki-page-message.yml @@ -0,0 +1,5 @@ +--- +title: Include commit message instead of entire page content in Wiki chat notifications +merge_request: 21722 +author: Ville Skyttä +type: changed diff --git a/danger/commit_messages/Dangerfile b/danger/commit_messages/Dangerfile index 60bc90139ab..a7466aa6ffb 100644 --- a/danger/commit_messages/Dangerfile +++ b/danger/commit_messages/Dangerfile @@ -59,12 +59,6 @@ def subject_starts_with_capital?(subject) first_char.upcase == first_char end -def ce_upstream? - return unless gitlab_danger.ci? - - gitlab.mr_labels.any? { |label| label == 'CE upstream' } -end - def too_many_changed_lines?(commit) commit.diff_parent.stats[:total][:files] > 3 && lines_changed_in_commit(commit) >= 30 @@ -291,11 +285,11 @@ def lint_commits(commits) end end -if count_filtered_commits(git.commits) > 10 && !ce_upstream? - warn( +lint_commits(git.commits) + +if count_filtered_commits(git.commits) > 10 + fail( 'This merge request includes more than 10 commits. ' \ 'Please rebase these commits into a smaller number of commits.' ) -else - lint_commits(git.commits) end diff --git a/db/migrate/20191217212348_add_modsecurity_enabled_to_ingress_application.rb b/db/migrate/20191217212348_add_modsecurity_enabled_to_ingress_application.rb new file mode 100644 index 00000000000..2690a5762dd --- /dev/null +++ b/db/migrate/20191217212348_add_modsecurity_enabled_to_ingress_application.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddModsecurityEnabledToIngressApplication < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def up + add_column :clusters_applications_ingress, :modsecurity_enabled, :boolean + end + + def down + remove_column :clusters_applications_ingress, :modsecurity_enabled + end +end diff --git a/db/schema.rb b/db/schema.rb index dee28ea4287..0f709482884 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1163,6 +1163,7 @@ ActiveRecord::Schema.define(version: 2019_12_18_225624) do t.text "status_reason" t.string "external_ip" t.string "external_hostname" + t.boolean "modsecurity_enabled" t.index ["cluster_id"], name: "index_clusters_applications_ingress_on_cluster_id", unique: true end diff --git a/doc/development/fe_guide/design_patterns.md b/doc/development/fe_guide/design_patterns.md index a7a0f39e2f3..72a7861ffcb 100644 --- a/doc/development/fe_guide/design_patterns.md +++ b/doc/development/fe_guide/design_patterns.md @@ -31,11 +31,11 @@ export default new MyThing(); export default class MyThing { constructor() { - if (!this.prototype.singleton) { + if (!MyThing.prototype.singleton) { this.init(); - this.prototype.singleton = this; + MyThing.prototype.singleton = this; } - return this.prototype.singleton; + return MyThing.prototype.singleton; } init() { diff --git a/lib/gitlab/error_tracking/detailed_error.rb b/lib/gitlab/error_tracking/detailed_error.rb index 169d6c03f12..a95e367c8b8 100644 --- a/lib/gitlab/error_tracking/detailed_error.rb +++ b/lib/gitlab/error_tracking/detailed_error.rb @@ -26,6 +26,7 @@ module Gitlab :project_slug, :short_id, :status, + :tags, :title, :type, :user_count diff --git a/lib/sentry/client/issue.rb b/lib/sentry/client/issue.rb index 08ed5392a11..28e87ab18a1 100644 --- a/lib/sentry/client/issue.rb +++ b/lib/sentry/client/issue.rb @@ -36,6 +36,7 @@ module Sentry id: issue.fetch('id'), first_seen: issue.fetch('firstSeen', nil), last_seen: issue.fetch('lastSeen', nil), + tags: extract_tags(issue), title: issue.fetch('title', nil), type: issue.fetch('type', nil), user_count: issue.fetch('userCount', nil), @@ -57,6 +58,13 @@ module Sentry last_release_short_version: issue.dig('lastRelease', 'shortVersion') ) end + + def extract_tags(issue) + { + level: issue.fetch('level', nil), + logger: issue.fetch('logger', nil) + } + end end end end diff --git a/spec/factories/error_tracking/detailed_error.rb b/spec/factories/error_tracking/detailed_error.rb index f12c327d403..b0114558bd4 100644 --- a/spec/factories/error_tracking/detailed_error.rb +++ b/spec/factories/error_tracking/detailed_error.rb @@ -18,6 +18,12 @@ FactoryBot.define do project_slug { 'project_name' } short_id { 'ID' } status { 'unresolved' } + tags do + { + level: 'error', + logger: 'rails' + } + end frequency do [ [Time.now.to_i, 10] diff --git a/spec/fixtures/api/schemas/error_tracking/error_detailed.json b/spec/fixtures/api/schemas/error_tracking/error_detailed.json index 2a1cd2c03e0..9a6797bf3c7 100644 --- a/spec/fixtures/api/schemas/error_tracking/error_detailed.json +++ b/spec/fixtures/api/schemas/error_tracking/error_detailed.json @@ -5,6 +5,7 @@ "external_base_url", "last_seen", "message", + "tags", "type", "title", "project_id", @@ -20,23 +21,38 @@ "last_release_short_version" ], "properties" : { - "id": { "type": "string"}, + "id": { "type": "string" }, "first_seen": { "type": "string", "format": "date-time" }, "last_seen": { "type": "string", "format": "date-time" }, "type": { "type": "string" }, "message": { "type": "string" }, "culprit": { "type": "string" }, - "count": { "type": "integer"}, + "count": { "type": "integer" }, "external_url": { "type": "string" }, "external_base_url": { "type": "string" }, "user_count": { "type": "integer"}, - "title": { "type": "string"}, - "project_id": { "type": "string"}, - "project_name": { "type": "string"}, - "project_slug": { "type": "string"}, - "short_id": { "type": "string"}, - "status": { "type": "string"}, - "frequency": { "type": "array"}, + "tags": { + "type": "object", + "required" : [ + "level", + "logger" + ], + "properties": { + "level": { + "type": "string" + }, + "logger": { + "type": "string" + } + } + }, + "title": { "type": "string" }, + "project_id": { "type": "string" }, + "project_name": { "type": "string" }, + "project_slug": { "type": "string" }, + "short_id": { "type": "string" }, + "status": { "type": "string" }, + "frequency": { "type": "array" }, "gitlab_issue": { "type": ["string", "null"] }, "first_release_last_commit": { "type": ["string", "null"] }, "last_release_last_commit": { "type": ["string", "null"] }, diff --git a/spec/frontend/behaviors/bind_in_out_spec.js b/spec/frontend/behaviors/bind_in_out_spec.js new file mode 100644 index 00000000000..923b6d372dd --- /dev/null +++ b/spec/frontend/behaviors/bind_in_out_spec.js @@ -0,0 +1,204 @@ +import BindInOut from '~/behaviors/bind_in_out'; +import ClassSpecHelper from '../helpers/class_spec_helper'; + +describe('BindInOut', () => { + let testContext; + + beforeEach(() => { + testContext = {}; + }); + + describe('constructor', () => { + beforeEach(() => { + testContext.in = {}; + testContext.out = {}; + + testContext.bindInOut = new BindInOut(testContext.in, testContext.out); + }); + + it('should set .in', () => { + expect(testContext.bindInOut.in).toBe(testContext.in); + }); + + it('should set .out', () => { + expect(testContext.bindInOut.out).toBe(testContext.out); + }); + + it('should set .eventWrapper', () => { + expect(testContext.bindInOut.eventWrapper).toEqual({}); + }); + + describe('if .in is an input', () => { + beforeEach(() => { + testContext.bindInOut = new BindInOut({ tagName: 'INPUT' }); + }); + + it('should set .eventType to keyup ', () => { + expect(testContext.bindInOut.eventType).toEqual('keyup'); + }); + }); + + describe('if .in is a textarea', () => { + beforeEach(() => { + testContext.bindInOut = new BindInOut({ tagName: 'TEXTAREA' }); + }); + + it('should set .eventType to keyup ', () => { + expect(testContext.bindInOut.eventType).toEqual('keyup'); + }); + }); + + describe('if .in is not an input or textarea', () => { + beforeEach(() => { + testContext.bindInOut = new BindInOut({ tagName: 'SELECT' }); + }); + + it('should set .eventType to change ', () => { + expect(testContext.bindInOut.eventType).toEqual('change'); + }); + }); + }); + + describe('addEvents', () => { + beforeEach(() => { + testContext.in = { + addEventListener: jest.fn(), + }; + + testContext.bindInOut = new BindInOut(testContext.in); + + testContext.addEvents = testContext.bindInOut.addEvents(); + }); + + it('should set .eventWrapper.updateOut', () => { + expect(testContext.bindInOut.eventWrapper.updateOut).toEqual(expect.any(Function)); + }); + + it('should call .addEventListener', () => { + expect(testContext.in.addEventListener).toHaveBeenCalledWith( + testContext.bindInOut.eventType, + testContext.bindInOut.eventWrapper.updateOut, + ); + }); + + it('should return the instance', () => { + expect(testContext.addEvents).toBe(testContext.bindInOut); + }); + }); + + describe('updateOut', () => { + beforeEach(() => { + testContext.in = { value: 'the-value' }; + testContext.out = { textContent: 'not-the-value' }; + + testContext.bindInOut = new BindInOut(testContext.in, testContext.out); + + testContext.updateOut = testContext.bindInOut.updateOut(); + }); + + it('should set .out.textContent to .in.value', () => { + expect(testContext.out.textContent).toBe(testContext.in.value); + }); + + it('should return the instance', () => { + expect(testContext.updateOut).toBe(testContext.bindInOut); + }); + }); + + describe('removeEvents', () => { + beforeEach(() => { + testContext.in = { + removeEventListener: jest.fn(), + }; + testContext.updateOut = () => {}; + + testContext.bindInOut = new BindInOut(testContext.in); + testContext.bindInOut.eventWrapper.updateOut = testContext.updateOut; + + testContext.removeEvents = testContext.bindInOut.removeEvents(); + }); + + it('should call .removeEventListener', () => { + expect(testContext.in.removeEventListener).toHaveBeenCalledWith( + testContext.bindInOut.eventType, + testContext.updateOut, + ); + }); + + it('should return the instance', () => { + expect(testContext.removeEvents).toBe(testContext.bindInOut); + }); + }); + + describe('initAll', () => { + beforeEach(() => { + testContext.ins = [0, 1, 2]; + testContext.instances = []; + + jest.spyOn(document, 'querySelectorAll').mockReturnValue(testContext.ins); + jest.spyOn(Array.prototype, 'map'); + jest.spyOn(BindInOut, 'init').mockImplementation(() => {}); + + testContext.initAll = BindInOut.initAll(); + }); + + ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'initAll'); + + it('should call .querySelectorAll', () => { + expect(document.querySelectorAll).toHaveBeenCalledWith('*[data-bind-in]'); + }); + + it('should call .map', () => { + expect(Array.prototype.map).toHaveBeenCalledWith(expect.any(Function)); + }); + + it('should call .init for each element', () => { + expect(BindInOut.init.mock.calls.length).toEqual(3); + }); + + it('should return an array of instances', () => { + expect(testContext.initAll).toEqual(expect.any(Array)); + }); + }); + + describe('init', () => { + beforeEach(() => { + // eslint-disable-next-line func-names + jest.spyOn(BindInOut.prototype, 'addEvents').mockImplementation(function() { + return this; + }); + // eslint-disable-next-line func-names + jest.spyOn(BindInOut.prototype, 'updateOut').mockImplementation(function() { + return this; + }); + + testContext.init = BindInOut.init({}, {}); + }); + + ClassSpecHelper.itShouldBeAStaticMethod(BindInOut, 'init'); + + it('should call .addEvents', () => { + expect(BindInOut.prototype.addEvents).toHaveBeenCalled(); + }); + + it('should call .updateOut', () => { + expect(BindInOut.prototype.updateOut).toHaveBeenCalled(); + }); + + describe('if no anOut is provided', () => { + beforeEach(() => { + testContext.anIn = { dataset: { bindIn: 'the-data-bind-in' } }; + + jest.spyOn(document, 'querySelector').mockImplementation(() => {}); + + BindInOut.init(testContext.anIn); + }); + + it('should call .querySelector', () => { + expect(document.querySelector).toHaveBeenCalledWith( + `*[data-bind-out="${testContext.anIn.dataset.bindIn}"]`, + ); + }); + }); + }); +}); diff --git a/spec/frontend/bootstrap_jquery_spec.js b/spec/frontend/bootstrap_jquery_spec.js new file mode 100644 index 00000000000..d5d592e3839 --- /dev/null +++ b/spec/frontend/bootstrap_jquery_spec.js @@ -0,0 +1,44 @@ +import $ from 'jquery'; +import '~/commons/bootstrap'; + +describe('Bootstrap jQuery extensions', () => { + describe('disable', () => { + beforeEach(() => { + setFixtures(''); + }); + + it('adds the disabled attribute', () => { + const $input = $('input').first(); + $input.disable(); + + expect($input).toHaveAttr('disabled', 'disabled'); + }); + + it('adds the disabled class', () => { + const $input = $('input').first(); + $input.disable(); + + expect($input).toHaveClass('disabled'); + }); + }); + + describe('enable', () => { + beforeEach(() => { + setFixtures(''); + }); + + it('removes the disabled attribute', () => { + const $input = $('input').first(); + $input.enable(); + + expect($input).not.toHaveAttr('disabled'); + }); + + it('removes the disabled class', () => { + const $input = $('input').first(); + $input.enable(); + + expect($input).not.toHaveClass('disabled'); + }); + }); +}); diff --git a/spec/frontend/branches/branches_delete_modal_spec.js b/spec/frontend/branches/branches_delete_modal_spec.js new file mode 100644 index 00000000000..21608feafc8 --- /dev/null +++ b/spec/frontend/branches/branches_delete_modal_spec.js @@ -0,0 +1,40 @@ +import $ from 'jquery'; +import DeleteModal from '~/branches/branches_delete_modal'; + +describe('branches delete modal', () => { + describe('setDisableDeleteButton', () => { + let submitSpy; + let $deleteButton; + + beforeEach(() => { + setFixtures(` + + `); + $deleteButton = $('.js-delete-branch'); + submitSpy = jest.fn(event => event.preventDefault()); + $('#modal-delete-branch form').on('submit', submitSpy); + // eslint-disable-next-line no-new + new DeleteModal(); + }); + + it('does not submit if button is disabled', () => { + $deleteButton.attr('disabled', true); + + $deleteButton.click(); + + expect(submitSpy).not.toHaveBeenCalled(); + }); + + it('submits if button is not disabled', () => { + $deleteButton.attr('disabled', false); + + $deleteButton.click(); + + expect(submitSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/breakpoints_spec.js b/spec/frontend/breakpoints_spec.js new file mode 100644 index 00000000000..c9014ddd3e2 --- /dev/null +++ b/spec/frontend/breakpoints_spec.js @@ -0,0 +1,27 @@ +import bp, { breakpoints } from '~/breakpoints'; + +describe('breakpoints', () => { + Object.keys(breakpoints).forEach(key => { + const size = breakpoints[key]; + + it(`returns ${key} when larger than ${size}`, () => { + jest.spyOn(bp, 'windowWidth').mockReturnValue(size + 10); + + expect(bp.getBreakpointSize()).toBe(key); + }); + }); + + describe('isDesktop', () => { + it('returns true when screen size is medium', () => { + jest.spyOn(bp, 'windowWidth').mockReturnValue(breakpoints.md + 10); + + expect(bp.isDesktop()).toBe(true); + }); + + it('returns false when screen size is small', () => { + jest.spyOn(bp, 'windowWidth').mockReturnValue(breakpoints.sm + 10); + + expect(bp.isDesktop()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/diffs/components/settings_dropdown_spec.js b/spec/frontend/diffs/components/settings_dropdown_spec.js new file mode 100644 index 00000000000..c360f5584ca --- /dev/null +++ b/spec/frontend/diffs/components/settings_dropdown_spec.js @@ -0,0 +1,167 @@ +import { mount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import diffModule from '~/diffs/store/modules'; +import SettingsDropdown from '~/diffs/components/settings_dropdown.vue'; +import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +describe('Diff settiings dropdown component', () => { + let vm; + let actions; + + function createComponent(extendStore = () => {}) { + const store = new Vuex.Store({ + modules: { + diffs: { + namespaced: true, + actions, + state: diffModule().state, + getters: diffModule().getters, + }, + }, + }); + + extendStore(store); + + vm = mount(localVue.extend(SettingsDropdown), { + localVue, + store, + sync: false, + }); + } + + beforeEach(() => { + actions = { + setInlineDiffViewType: jest.fn(), + setParallelDiffViewType: jest.fn(), + setRenderTreeList: jest.fn(), + setShowWhitespace: jest.fn(), + }; + }); + + afterEach(() => { + vm.destroy(); + }); + + describe('tree view buttons', () => { + it('list view button dispatches setRenderTreeList with false', () => { + createComponent(); + + vm.find('.js-list-view').trigger('click'); + + expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), false, undefined); + }); + + it('tree view button dispatches setRenderTreeList with true', () => { + createComponent(); + + vm.find('.js-tree-view').trigger('click'); + + expect(actions.setRenderTreeList).toHaveBeenCalledWith(expect.anything(), true, undefined); + }); + + it('sets list button as active when renderTreeList is false', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + renderTreeList: false, + }); + }); + + expect(vm.find('.js-list-view').classes('active')).toBe(true); + expect(vm.find('.js-tree-view').classes('active')).toBe(false); + }); + + it('sets tree button as active when renderTreeList is true', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + renderTreeList: true, + }); + }); + + expect(vm.find('.js-list-view').classes('active')).toBe(false); + expect(vm.find('.js-tree-view').classes('active')).toBe(true); + }); + }); + + describe('compare changes', () => { + it('sets inline button as active', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + diffViewType: INLINE_DIFF_VIEW_TYPE, + }); + }); + + expect(vm.find('.js-inline-diff-button').classes('active')).toBe(true); + expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(false); + }); + + it('sets parallel button as active', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + diffViewType: PARALLEL_DIFF_VIEW_TYPE, + }); + }); + + expect(vm.find('.js-inline-diff-button').classes('active')).toBe(false); + expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(true); + }); + + it('calls setInlineDiffViewType when clicking inline button', () => { + createComponent(); + + vm.find('.js-inline-diff-button').trigger('click'); + + expect(actions.setInlineDiffViewType).toHaveBeenCalled(); + }); + + it('calls setParallelDiffViewType when clicking parallel button', () => { + createComponent(); + + vm.find('.js-parallel-diff-button').trigger('click'); + + expect(actions.setParallelDiffViewType).toHaveBeenCalled(); + }); + }); + + describe('whitespace toggle', () => { + it('does not set as checked when showWhitespace is false', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + showWhitespace: false, + }); + }); + + expect(vm.find('#show-whitespace').element.checked).toBe(false); + }); + + it('sets as checked when showWhitespace is true', () => { + createComponent(store => { + Object.assign(store.state.diffs, { + showWhitespace: true, + }); + }); + + expect(vm.find('#show-whitespace').element.checked).toBe(true); + }); + + it('calls setShowWhitespace on change', () => { + createComponent(); + + const checkbox = vm.find('#show-whitespace'); + + checkbox.element.checked = true; + checkbox.trigger('change'); + + expect(actions.setShowWhitespace).toHaveBeenCalledWith( + expect.anything(), + { + showWhitespace: true, + pushState: true, + }, + undefined, + ); + }); + }); +}); diff --git a/spec/frontend/droplab/constants_spec.js b/spec/frontend/droplab/constants_spec.js new file mode 100644 index 00000000000..fd48228d6a2 --- /dev/null +++ b/spec/frontend/droplab/constants_spec.js @@ -0,0 +1,39 @@ +import * as constants from '~/droplab/constants'; + +describe('constants', () => { + describe('DATA_TRIGGER', () => { + it('should be `data-dropdown-trigger`', () => { + expect(constants.DATA_TRIGGER).toBe('data-dropdown-trigger'); + }); + }); + + describe('DATA_DROPDOWN', () => { + it('should be `data-dropdown`', () => { + expect(constants.DATA_DROPDOWN).toBe('data-dropdown'); + }); + }); + + describe('SELECTED_CLASS', () => { + it('should be `droplab-item-selected`', () => { + expect(constants.SELECTED_CLASS).toBe('droplab-item-selected'); + }); + }); + + describe('ACTIVE_CLASS', () => { + it('should be `droplab-item-active`', () => { + expect(constants.ACTIVE_CLASS).toBe('droplab-item-active'); + }); + }); + + describe('TEMPLATE_REGEX', () => { + it('should be a handlebars templating syntax regex', () => { + expect(constants.TEMPLATE_REGEX).toEqual(/\{\{(.+?)\}\}/g); + }); + }); + + describe('IGNORE_CLASS', () => { + it('should be `droplab-item-ignore`', () => { + expect(constants.IGNORE_CLASS).toBe('droplab-item-ignore'); + }); + }); +}); diff --git a/spec/frontend/droplab/plugins/ajax_filter_spec.js b/spec/frontend/droplab/plugins/ajax_filter_spec.js new file mode 100644 index 00000000000..5ec0400cbc5 --- /dev/null +++ b/spec/frontend/droplab/plugins/ajax_filter_spec.js @@ -0,0 +1,72 @@ +import AjaxCache from '~/lib/utils/ajax_cache'; +import AjaxFilter from '~/droplab/plugins/ajax_filter'; + +describe('AjaxFilter', () => { + let dummyConfig; + const dummyData = 'dummy data'; + let dummyList; + + beforeEach(() => { + dummyConfig = { + endpoint: 'dummy endpoint', + searchKey: 'dummy search key', + }; + dummyList = { + data: [], + list: document.createElement('div'), + }; + + AjaxFilter.hook = { + config: { + AjaxFilter: dummyConfig, + }, + list: dummyList, + }; + }); + + describe('trigger', () => { + let ajaxSpy; + + beforeEach(() => { + jest.spyOn(AjaxCache, 'retrieve').mockImplementation(url => ajaxSpy(url)); + jest.spyOn(AjaxFilter, '_loadData').mockImplementation(() => {}); + + dummyConfig.onLoadingFinished = jest.fn(); + + const dynamicList = document.createElement('div'); + dynamicList.dataset.dynamic = true; + dummyList.list.appendChild(dynamicList); + }); + + it('calls onLoadingFinished after loading data', done => { + ajaxSpy = url => { + expect(url).toBe('dummy endpoint?dummy search key='); + return Promise.resolve(dummyData); + }; + + AjaxFilter.trigger() + .then(() => { + expect(dummyConfig.onLoadingFinished.mock.calls.length).toBe(1); + }) + .then(done) + .catch(done.fail); + }); + + it('does not call onLoadingFinished if Ajax call fails', done => { + const dummyError = new Error('My dummy is sick! :-('); + ajaxSpy = url => { + expect(url).toBe('dummy endpoint?dummy search key='); + return Promise.reject(dummyError); + }; + + AjaxFilter.trigger() + .then(done.fail) + .catch(error => { + expect(error).toBe(dummyError); + expect(dummyConfig.onLoadingFinished.mock.calls.length).toBe(0); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/frontend/droplab/plugins/ajax_spec.js b/spec/frontend/droplab/plugins/ajax_spec.js new file mode 100644 index 00000000000..1d7576ce420 --- /dev/null +++ b/spec/frontend/droplab/plugins/ajax_spec.js @@ -0,0 +1,41 @@ +import AjaxCache from '~/lib/utils/ajax_cache'; +import Ajax from '~/droplab/plugins/ajax'; + +describe('Ajax', () => { + describe('preprocessing', () => { + const config = {}; + + describe('is not configured', () => { + it('passes the data through', () => { + const data = ['data']; + + expect(Ajax.preprocessing(config, data)).toEqual(data); + }); + }); + + describe('is configured', () => { + const processedArray = ['processed']; + + beforeEach(() => { + config.preprocessing = () => processedArray; + jest.spyOn(config, 'preprocessing').mockImplementation(() => processedArray); + }); + + it('calls preprocessing', () => { + Ajax.preprocessing(config, []); + + expect(config.preprocessing.mock.calls.length).toBe(1); + }); + + it('overrides AjaxCache', () => { + jest.spyOn(AjaxCache, 'override').mockImplementation((endpoint, results) => { + expect(results).toEqual(processedArray); + }); + + Ajax.preprocessing(config, []); + + expect(AjaxCache.override.mock.calls.length).toBe(1); + }); + }); + }); +}); diff --git a/spec/frontend/feature_highlight/feature_highlight_options_spec.js b/spec/frontend/feature_highlight/feature_highlight_options_spec.js new file mode 100644 index 00000000000..cd41d1ed091 --- /dev/null +++ b/spec/frontend/feature_highlight/feature_highlight_options_spec.js @@ -0,0 +1,30 @@ +import domContentLoaded from '~/feature_highlight/feature_highlight_options'; +import bp from '~/breakpoints'; + +describe('feature highlight options', () => { + describe('domContentLoaded', () => { + it('should not call highlightFeatures when breakpoint is xs', () => { + jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('xs'); + + expect(domContentLoaded()).toBe(false); + }); + + it('should not call highlightFeatures when breakpoint is sm', () => { + jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('sm'); + + expect(domContentLoaded()).toBe(false); + }); + + it('should not call highlightFeatures when breakpoint is md', () => { + jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('md'); + + expect(domContentLoaded()).toBe(false); + }); + + it('should call highlightFeatures when breakpoint is lg', () => { + jest.spyOn(bp, 'getBreakpointSize').mockReturnValue('lg'); + + expect(domContentLoaded()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js new file mode 100644 index 00000000000..2543fb8768b --- /dev/null +++ b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js @@ -0,0 +1,201 @@ +import Vue from 'vue'; +import eventHub from '~/filtered_search/event_hub'; +import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; + +const createComponent = propsData => { + const Component = Vue.extend(RecentSearchesDropdownContent); + + return new Component({ + el: document.createElement('div'), + propsData, + }); +}; + +// Remove all the newlines and whitespace from the formatted markup +const trimMarkupWhitespace = text => text.replace(/(\n|\s)+/gm, ' ').trim(); + +describe('RecentSearchesDropdownContent', () => { + const propsDataWithoutItems = { + items: [], + allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(), + }; + const propsDataWithItems = { + items: ['foo', 'author:@root label:~foo bar'], + allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(), + }; + + let vm; + afterEach(() => { + if (vm) { + vm.$destroy(); + } + }); + + describe('with no items', () => { + let el; + + beforeEach(() => { + vm = createComponent(propsDataWithoutItems); + el = vm.$el; + }); + + it('should render empty state', () => { + expect(el.querySelector('.dropdown-info-note')).toBeDefined(); + + const items = el.querySelectorAll('.filtered-search-history-dropdown-item'); + + expect(items.length).toEqual(propsDataWithoutItems.items.length); + }); + }); + + describe('with items', () => { + let el; + + beforeEach(() => { + vm = createComponent(propsDataWithItems); + el = vm.$el; + }); + + it('should render clear recent searches button', () => { + expect(el.querySelector('.filtered-search-history-clear-button')).toBeDefined(); + }); + + it('should render recent search items', () => { + const items = el.querySelectorAll('.filtered-search-history-dropdown-item'); + + expect(items.length).toEqual(propsDataWithItems.items.length); + + expect( + trimMarkupWhitespace( + items[0].querySelector('.filtered-search-history-dropdown-search-token').textContent, + ), + ).toEqual('foo'); + + const item1Tokens = items[1].querySelectorAll('.filtered-search-history-dropdown-token'); + + expect(item1Tokens.length).toEqual(2); + expect(item1Tokens[0].querySelector('.name').textContent).toEqual('author:'); + expect(item1Tokens[0].querySelector('.value').textContent).toEqual('@root'); + expect(item1Tokens[1].querySelector('.name').textContent).toEqual('label:'); + expect(item1Tokens[1].querySelector('.value').textContent).toEqual('~foo'); + expect( + trimMarkupWhitespace( + items[1].querySelector('.filtered-search-history-dropdown-search-token').textContent, + ), + ).toEqual('bar'); + }); + }); + + describe('if isLocalStorageAvailable is `false`', () => { + let el; + + beforeEach(() => { + const props = Object.assign({ isLocalStorageAvailable: false }, propsDataWithItems); + + vm = createComponent(props); + el = vm.$el; + }); + + it('should render an info note', () => { + const note = el.querySelector('.dropdown-info-note'); + const items = el.querySelectorAll('.filtered-search-history-dropdown-item'); + + expect(note).toBeDefined(); + expect(note.innerText.trim()).toBe('This feature requires local storage to be enabled'); + expect(items.length).toEqual(propsDataWithoutItems.items.length); + }); + }); + + describe('computed', () => { + describe('processedItems', () => { + it('with items', () => { + vm = createComponent(propsDataWithItems); + const { processedItems } = vm; + + expect(processedItems.length).toEqual(2); + + expect(processedItems[0].text).toEqual(propsDataWithItems.items[0]); + expect(processedItems[0].tokens).toEqual([]); + expect(processedItems[0].searchToken).toEqual('foo'); + + expect(processedItems[1].text).toEqual(propsDataWithItems.items[1]); + expect(processedItems[1].tokens.length).toEqual(2); + expect(processedItems[1].tokens[0].prefix).toEqual('author:'); + expect(processedItems[1].tokens[0].suffix).toEqual('@root'); + expect(processedItems[1].tokens[1].prefix).toEqual('label:'); + expect(processedItems[1].tokens[1].suffix).toEqual('~foo'); + expect(processedItems[1].searchToken).toEqual('bar'); + }); + + it('with no items', () => { + vm = createComponent(propsDataWithoutItems); + const { processedItems } = vm; + + expect(processedItems.length).toEqual(0); + }); + }); + + describe('hasItems', () => { + it('with items', () => { + vm = createComponent(propsDataWithItems); + const { hasItems } = vm; + + expect(hasItems).toEqual(true); + }); + + it('with no items', () => { + vm = createComponent(propsDataWithoutItems); + const { hasItems } = vm; + + expect(hasItems).toEqual(false); + }); + }); + }); + + describe('methods', () => { + describe('onItemActivated', () => { + let onRecentSearchesItemSelectedSpy; + + beforeEach(() => { + onRecentSearchesItemSelectedSpy = jest.fn(); + eventHub.$on('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy); + + vm = createComponent(propsDataWithItems); + }); + + afterEach(() => { + eventHub.$off('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy); + }); + + it('emits event', () => { + expect(onRecentSearchesItemSelectedSpy).not.toHaveBeenCalled(); + vm.onItemActivated('something'); + + expect(onRecentSearchesItemSelectedSpy).toHaveBeenCalledWith('something'); + }); + }); + + describe('onRequestClearRecentSearches', () => { + let onRequestClearRecentSearchesSpy; + + beforeEach(() => { + onRequestClearRecentSearchesSpy = jest.fn(); + eventHub.$on('requestClearRecentSearches', onRequestClearRecentSearchesSpy); + + vm = createComponent(propsDataWithItems); + }); + + afterEach(() => { + eventHub.$off('requestClearRecentSearches', onRequestClearRecentSearchesSpy); + }); + + it('emits event', () => { + expect(onRequestClearRecentSearchesSpy).not.toHaveBeenCalled(); + vm.onRequestClearRecentSearches({ stopPropagation: () => {} }); + + expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/spec/frontend/filtered_search/dropdown_user_spec.js b/spec/frontend/filtered_search/dropdown_user_spec.js new file mode 100644 index 00000000000..8eef10290bf --- /dev/null +++ b/spec/frontend/filtered_search/dropdown_user_spec.js @@ -0,0 +1,113 @@ +import DropdownUtils from '~/filtered_search/dropdown_utils'; +import DropdownUser from '~/filtered_search/dropdown_user'; +import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer'; +import IssuableFilteredTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; + +describe('Dropdown User', () => { + describe('getSearchInput', () => { + let dropdownUser; + + beforeEach(() => { + jest.spyOn(DropdownUser.prototype, 'bindEvents').mockImplementation(() => {}); + jest.spyOn(DropdownUser.prototype, 'getProjectId').mockImplementation(() => {}); + jest.spyOn(DropdownUser.prototype, 'getGroupId').mockImplementation(() => {}); + jest.spyOn(DropdownUtils, 'getSearchInput').mockImplementation(() => {}); + + dropdownUser = new DropdownUser({ + tokenKeys: IssuableFilteredTokenKeys, + }); + }); + + it('should not return the double quote found in value', () => { + jest.spyOn(FilteredSearchTokenizer, 'processTokens').mockReturnValue({ + lastToken: '"johnny appleseed', + }); + + expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); + }); + + it('should not return the single quote found in value', () => { + jest.spyOn(FilteredSearchTokenizer, 'processTokens').mockReturnValue({ + lastToken: "'larry boy", + }); + + expect(dropdownUser.getSearchInput()).toBe('larry boy'); + }); + }); + + describe("config AjaxFilter's endpoint", () => { + beforeEach(() => { + jest.spyOn(DropdownUser.prototype, 'bindEvents').mockImplementation(() => {}); + jest.spyOn(DropdownUser.prototype, 'getProjectId').mockImplementation(() => {}); + jest.spyOn(DropdownUser.prototype, 'getGroupId').mockImplementation(() => {}); + }); + + it('should return endpoint', () => { + window.gon = { + relative_url_root: '', + }; + const dropdown = new DropdownUser(); + + expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); + }); + + it('should return endpoint when relative_url_root is undefined', () => { + const dropdown = new DropdownUser(); + + expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); + }); + + it('should return endpoint with relative url when available', () => { + window.gon = { + relative_url_root: '/gitlab_directory', + }; + const dropdown = new DropdownUser(); + + expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); + }); + + afterEach(() => { + window.gon = {}; + }); + }); + + describe('hideCurrentUser', () => { + const fixtureTemplate = 'issues/issue_list.html'; + preloadFixtures(fixtureTemplate); + + let dropdown; + let authorFilterDropdownElement; + + beforeEach(() => { + loadFixtures(fixtureTemplate); + authorFilterDropdownElement = document.querySelector('#js-dropdown-author'); + const dummyInput = document.createElement('div'); + dropdown = new DropdownUser({ + dropdown: authorFilterDropdownElement, + input: dummyInput, + }); + }); + + const findCurrentUserElement = () => + authorFilterDropdownElement.querySelector('.js-current-user'); + + it('hides the current user from dropdown', () => { + const currentUserElement = findCurrentUserElement(); + + expect(currentUserElement).not.toBe(null); + + dropdown.hideCurrentUser(); + + expect(currentUserElement.classList).toContain('hidden'); + }); + + it('does nothing if no user is logged in', () => { + const currentUserElement = findCurrentUserElement(); + currentUserElement.parentNode.removeChild(currentUserElement); + + expect(findCurrentUserElement()).toBe(null); + + dropdown.hideCurrentUser(); + }); + }); +}); diff --git a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js new file mode 100644 index 00000000000..e5f1ab21c7f --- /dev/null +++ b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js @@ -0,0 +1,83 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue'; +import eventHub from '~/frequent_items/event_hub'; + +const localVue = createLocalVue(); + +const createComponent = (namespace = 'projects') => + shallowMount(localVue.extend(searchComponent), { + propsData: { namespace }, + localVue, + sync: false, + }); + +describe('FrequentItemsSearchInputComponent', () => { + let wrapper; + let vm; + + beforeEach(() => { + wrapper = createComponent(); + + ({ vm } = wrapper); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('methods', () => { + describe('setFocus', () => { + it('should set focus to search input', () => { + jest.spyOn(vm.$refs.search, 'focus').mockImplementation(() => {}); + + vm.setFocus(); + + expect(vm.$refs.search.focus).toHaveBeenCalled(); + }); + }); + }); + + describe('mounted', () => { + it('should listen `dropdownOpen` event', done => { + jest.spyOn(eventHub, '$on').mockImplementation(() => {}); + const vmX = createComponent().vm; + + localVue.nextTick(() => { + expect(eventHub.$on).toHaveBeenCalledWith( + `${vmX.namespace}-dropdownOpen`, + expect.any(Function), + ); + done(); + }); + }); + }); + + describe('beforeDestroy', () => { + it('should unbind event listeners on eventHub', done => { + const vmX = createComponent().vm; + jest.spyOn(eventHub, '$off').mockImplementation(() => {}); + + vmX.$mount(); + vmX.$destroy(); + + localVue.nextTick(() => { + expect(eventHub.$off).toHaveBeenCalledWith( + `${vmX.namespace}-dropdownOpen`, + expect.any(Function), + ); + done(); + }); + }); + }); + + describe('template', () => { + it('should render component element', () => { + expect(wrapper.classes()).toContain('search-input-container'); + expect(wrapper.contains('input.form-control')).toBe(true); + expect(wrapper.contains('.search-icon')).toBe(true); + expect(wrapper.find('input.form-control').attributes('placeholder')).toBe( + 'Search your projects', + ); + }); + }); +}); diff --git a/spec/frontend/gl_field_errors_spec.js b/spec/frontend/gl_field_errors_spec.js new file mode 100644 index 00000000000..4653f519f65 --- /dev/null +++ b/spec/frontend/gl_field_errors_spec.js @@ -0,0 +1,144 @@ +/* eslint-disable arrow-body-style */ + +import $ from 'jquery'; +import GlFieldErrors from '~/gl_field_errors'; + +describe('GL Style Field Errors', () => { + let testContext; + + beforeEach(() => { + testContext = {}; + }); + + preloadFixtures('static/gl_field_errors.html'); + + beforeEach(() => { + loadFixtures('static/gl_field_errors.html'); + const $form = $('form.gl-show-field-errors'); + + testContext.$form = $form; + testContext.fieldErrors = new GlFieldErrors($form); + }); + + it('should select the correct input elements', () => { + expect(testContext.$form).toBeDefined(); + expect(testContext.$form.length).toBe(1); + expect(testContext.fieldErrors).toBeDefined(); + const { inputs } = testContext.fieldErrors.state; + + expect(inputs.length).toBe(4); + }); + + it('should ignore elements with custom error handling', () => { + const customErrorFlag = 'gl-field-error-ignore'; + const customErrorElem = $(`.${customErrorFlag}`); + + expect(customErrorElem.length).toBe(1); + + const customErrors = testContext.fieldErrors.state.inputs.filter(input => { + return input.inputElement.hasClass(customErrorFlag); + }); + + expect(customErrors.length).toBe(0); + }); + + it('should not show any errors before submit attempt', () => { + testContext.$form + .find('.email') + .val('not-a-valid-email') + .keyup(); + testContext.$form + .find('.text-required') + .val('') + .keyup(); + testContext.$form + .find('.alphanumberic') + .val('?---*') + .keyup(); + + const errorsShown = testContext.$form.find('.gl-field-error-outline'); + + expect(errorsShown.length).toBe(0); + }); + + it('should show errors when input valid is submitted', () => { + testContext.$form + .find('.email') + .val('not-a-valid-email') + .keyup(); + testContext.$form + .find('.text-required') + .val('') + .keyup(); + testContext.$form + .find('.alphanumberic') + .val('?---*') + .keyup(); + + testContext.$form.submit(); + + const errorsShown = testContext.$form.find('.gl-field-error-outline'); + + expect(errorsShown.length).toBe(4); + }); + + it('should properly track validity state on input after invalid submission attempt', () => { + testContext.$form.submit(); + + const emailInputModel = testContext.fieldErrors.state.inputs[1]; + const fieldState = emailInputModel.state; + const emailInputElement = emailInputModel.inputElement; + + // No input + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(true); + expect(fieldState.valid).toBe(false); + + // Then invalid input + emailInputElement.val('not-a-valid-email').keyup(); + + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(false); + + // Then valid input + emailInputElement.val('email@gitlab.com').keyup(); + + expect(emailInputElement).not.toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(true); + + // Then invalid input + emailInputElement.val('not-a-valid-email').keyup(); + + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(false); + + // Then empty input + emailInputElement.val('').keyup(); + + expect(emailInputElement).toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(true); + expect(fieldState.valid).toBe(false); + + // Then valid input + emailInputElement.val('email@gitlab.com').keyup(); + + expect(emailInputElement).not.toHaveClass('gl-field-error-outline'); + expect(fieldState.empty).toBe(false); + expect(fieldState.valid).toBe(true); + }); + + it('should properly infer error messages', () => { + testContext.$form.submit(); + const trackedInputs = testContext.fieldErrors.state.inputs; + const inputHasTitle = trackedInputs[1]; + const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error'); + const inputNoTitle = trackedInputs[2]; + const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error'); + + expect(noTitleErrorElem.text()).toBe('This field is required.'); + expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.'); + }); +}); diff --git a/spec/frontend/gpg_badges_spec.js b/spec/frontend/gpg_badges_spec.js new file mode 100644 index 00000000000..809cc5c88e2 --- /dev/null +++ b/spec/frontend/gpg_badges_spec.js @@ -0,0 +1,92 @@ +import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'spec/test_constants'; +import axios from '~/lib/utils/axios_utils'; +import GpgBadges from '~/gpg_badges'; + +describe('GpgBadges', () => { + let mock; + const dummyCommitSha = 'n0m0rec0ffee'; + const dummyBadgeHtml = 'dummy html'; + const dummyResponse = { + signatures: [ + { + commit_sha: dummyCommitSha, + html: dummyBadgeHtml, + }, + ], + }; + const dummyUrl = `${TEST_HOST}/dummy/signatures`; + + beforeEach(() => { + mock = new MockAdapter(axios); + setFixtures(` +
+ + +
+
+
+
+ `); + }); + + afterEach(() => { + mock.restore(); + }); + + it('does not make a request if there is no container element', done => { + setFixtures(''); + jest.spyOn(axios, 'get').mockImplementation(() => {}); + + GpgBadges.fetch() + .then(() => { + expect(axios.get).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('throws an error if the endpoint is missing', done => { + setFixtures('
'); + jest.spyOn(axios, 'get').mockImplementation(() => {}); + + GpgBadges.fetch() + .then(() => done.fail('Expected error to be thrown')) + .catch(error => { + expect(error.message).toBe('Missing commit signatures endpoint!'); + expect(axios.get).not.toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('displays a loading spinner', done => { + mock.onGet(dummyUrl).replyOnce(200); + + GpgBadges.fetch() + .then(() => { + expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null); + const spinners = document.querySelectorAll('.js-loading-gpg-badge i.fa.fa-spinner.fa-spin'); + + expect(spinners.length).toBe(1); + done(); + }) + .catch(done.fail); + }); + + it('replaces the loading spinner', done => { + mock.onGet(dummyUrl).replyOnce(200, dummyResponse); + + GpgBadges.fetch() + .then(() => { + expect(document.querySelector('.js-loading-gpg-badge')).toBe(null); + const parentContainer = document.querySelector('.parent-container'); + + expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml); + done(); + }) + .catch(done.fail); + }); +}); diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js new file mode 100644 index 00000000000..00b5b306d66 --- /dev/null +++ b/spec/frontend/header_spec.js @@ -0,0 +1,53 @@ +import $ from 'jquery'; +import initTodoToggle from '~/header'; + +describe('Header', () => { + const todosPendingCount = '.todos-count'; + const fixtureTemplate = 'issues/open-issue.html'; + + function isTodosCountHidden() { + return $(todosPendingCount).hasClass('hidden'); + } + + function triggerToggle(newCount) { + $(document).trigger('todo:toggle', newCount); + } + + preloadFixtures(fixtureTemplate); + beforeEach(() => { + initTodoToggle(); + loadFixtures(fixtureTemplate); + }); + + it('should update todos-count after receiving the todo:toggle event', () => { + triggerToggle(5); + + expect($(todosPendingCount).text()).toEqual('5'); + }); + + it('should hide todos-count when it is 0', () => { + triggerToggle(0); + + expect(isTodosCountHidden()).toEqual(true); + }); + + it('should show todos-count when it is more than 0', () => { + triggerToggle(10); + + expect(isTodosCountHidden()).toEqual(false); + }); + + describe('when todos-count is 1000', () => { + beforeEach(() => { + triggerToggle(1000); + }); + + it('should show todos-count', () => { + expect(isTodosCountHidden()).toEqual(false); + }); + + it('should show 99+ for todos-count', () => { + expect($(todosPendingCount).text()).toEqual('99+'); + }); + }); +}); diff --git a/spec/frontend/helpers/class_spec_helper_spec.js b/spec/frontend/helpers/class_spec_helper_spec.js new file mode 100644 index 00000000000..533d5687bde --- /dev/null +++ b/spec/frontend/helpers/class_spec_helper_spec.js @@ -0,0 +1,26 @@ +/* global ClassSpecHelper */ + +import './class_spec_helper'; + +describe('ClassSpecHelper', () => { + let testContext; + + beforeEach(() => { + testContext = {}; + }); + + describe('itShouldBeAStaticMethod', () => { + beforeEach(() => { + class TestClass { + instanceMethod() { + this.prop = 'val'; + } + static staticMethod() {} + } + + testContext.TestClass = TestClass; + }); + + ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod'); + }); +}); diff --git a/spec/frontend/ide/components/commit_sidebar/stage_button_spec.js b/spec/frontend/ide/components/commit_sidebar/stage_button_spec.js new file mode 100644 index 00000000000..b59de4dac0e --- /dev/null +++ b/spec/frontend/ide/components/commit_sidebar/stage_button_spec.js @@ -0,0 +1,46 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import stageButton from '~/ide/components/commit_sidebar/stage_button.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file, resetStore } from '../../helpers'; + +describe('IDE stage file button', () => { + let vm; + let f; + + beforeEach(() => { + const Component = Vue.extend(stageButton); + f = file(); + + vm = createComponentWithStore(Component, store, { + path: f.path, + }); + + jest.spyOn(vm, 'stageChange').mockImplementation(() => {}); + jest.spyOn(vm, 'discardFileChanges').mockImplementation(() => {}); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders button to discard & stage', () => { + expect(vm.$el.querySelectorAll('.btn-blank').length).toBe(2); + }); + + it('calls store with stage button', () => { + vm.$el.querySelectorAll('.btn')[0].click(); + + expect(vm.stageChange).toHaveBeenCalledWith(f.path); + }); + + it('calls store with discard button', () => { + vm.$el.querySelector('.btn-danger').click(); + + expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path); + }); +}); diff --git a/spec/frontend/ide/components/commit_sidebar/unstage_button_spec.js b/spec/frontend/ide/components/commit_sidebar/unstage_button_spec.js new file mode 100644 index 00000000000..53b53c8c815 --- /dev/null +++ b/spec/frontend/ide/components/commit_sidebar/unstage_button_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import unstageButton from '~/ide/components/commit_sidebar/unstage_button.vue'; +import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; +import { file, resetStore } from '../../helpers'; + +describe('IDE unstage file button', () => { + let vm; + let f; + + beforeEach(() => { + const Component = Vue.extend(unstageButton); + f = file(); + + vm = createComponentWithStore(Component, store, { + path: f.path, + }); + + jest.spyOn(vm, 'unstageChange').mockImplementation(() => {}); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + + resetStore(vm.$store); + }); + + it('renders button to unstage', () => { + expect(vm.$el.querySelectorAll('.btn').length).toBe(1); + }); + + it('calls store with unnstage button', () => { + vm.$el.querySelector('.btn').click(); + + expect(vm.unstageChange).toHaveBeenCalledWith(f.path); + }); +}); diff --git a/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js new file mode 100644 index 00000000000..096851a5401 --- /dev/null +++ b/spec/frontend/ide/components/jobs/detail/scroll_button_spec.js @@ -0,0 +1,59 @@ +import Vue from 'vue'; +import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue'; +import mountComponent from '../../../../helpers/vue_mount_component_helper'; + +describe('IDE job log scroll button', () => { + const Component = Vue.extend(ScrollButton); + let vm; + + beforeEach(() => { + vm = mountComponent(Component, { + direction: 'up', + disabled: false, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('iconName', () => { + ['up', 'down'].forEach(direction => { + it(`returns icon name for ${direction}`, () => { + vm.direction = direction; + + expect(vm.iconName).toBe(`scroll_${direction}`); + }); + }); + }); + + describe('tooltipTitle', () => { + it('returns title for up', () => { + expect(vm.tooltipTitle).toBe('Scroll to top'); + }); + + it('returns title for down', () => { + vm.direction = 'down'; + + expect(vm.tooltipTitle).toBe('Scroll to bottom'); + }); + }); + + it('emits click event on click', () => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + + vm.$el.querySelector('.btn-scroll').click(); + + expect(vm.$emit).toHaveBeenCalledWith('click'); + }); + + it('disables button when disabled is true', done => { + vm.disabled = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.btn-scroll').hasAttribute('disabled')).toBe(true); + + done(); + }); + }); +}); diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js new file mode 100644 index 00000000000..283ea266821 --- /dev/null +++ b/spec/frontend/ide/stores/actions/file_spec.js @@ -0,0 +1,805 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import store from '~/ide/stores'; +import * as actions from '~/ide/stores/actions/file'; +import * as types from '~/ide/stores/mutation_types'; +import service from '~/ide/services'; +import router from '~/ide/ide_router'; +import eventHub from '~/ide/eventhub'; +import { file, resetStore } from '../../helpers'; +import testAction from '../../../helpers/vuex_action_helper'; + +const RELATIVE_URL_ROOT = '/gitlab'; + +describe('IDE store file actions', () => { + let mock; + let originalGon; + + beforeEach(() => { + mock = new MockAdapter(axios); + originalGon = window.gon; + window.gon = { + ...window.gon, + relative_url_root: RELATIVE_URL_ROOT, + }; + + jest.spyOn(router, 'push').mockImplementation(() => {}); + }); + + afterEach(() => { + mock.restore(); + resetStore(store); + window.gon = originalGon; + }); + + describe('closeFile', () => { + let localFile; + + beforeEach(() => { + localFile = file('testFile'); + localFile.active = true; + localFile.opened = true; + localFile.parentTreeUrl = 'parentTreeUrl'; + + store.state.openFiles.push(localFile); + store.state.entries[localFile.path] = localFile; + }); + + it('closes open files', done => { + store + .dispatch('closeFile', localFile) + .then(() => { + expect(localFile.opened).toBeFalsy(); + expect(localFile.active).toBeFalsy(); + expect(store.state.openFiles.length).toBe(0); + + done(); + }) + .catch(done.fail); + }); + + it('closes file even if file has changes', done => { + store.state.changedFiles.push(localFile); + + store + .dispatch('closeFile', localFile) + .then(Vue.nextTick) + .then(() => { + expect(store.state.openFiles.length).toBe(0); + expect(store.state.changedFiles.length).toBe(1); + + done(); + }) + .catch(done.fail); + }); + + it('closes file & opens next available file', done => { + const f = { + ...file('newOpenFile'), + url: '/newOpenFile', + }; + + store.state.openFiles.push(f); + store.state.entries[f.path] = f; + + store + .dispatch('closeFile', localFile) + .then(Vue.nextTick) + .then(() => { + expect(router.push).toHaveBeenCalledWith(`/project${f.url}`); + + done(); + }) + .catch(done.fail); + }); + + it('removes file if it pending', done => { + store.state.openFiles.push({ + ...localFile, + pending: true, + }); + + store + .dispatch('closeFile', localFile) + .then(() => { + expect(store.state.openFiles.length).toBe(0); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('setFileActive', () => { + let localFile; + let scrollToTabSpy; + let oldScrollToTab; + + beforeEach(() => { + scrollToTabSpy = jest.fn(); + oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line + store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line + + localFile = file('setThisActive'); + + store.state.entries[localFile.path] = localFile; + }); + + afterEach(() => { + store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line + }); + + it('calls scrollToTab', () => { + const dispatch = jest.fn(); + + actions.setFileActive( + { commit() {}, state: store.state, getters: store.getters, dispatch }, + localFile.path, + ); + + expect(dispatch).toHaveBeenCalledWith('scrollToTab'); + }); + + it('commits SET_FILE_ACTIVE', () => { + const commit = jest.fn(); + + actions.setFileActive( + { commit, state: store.state, getters: store.getters, dispatch() {} }, + localFile.path, + ); + + expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', { + path: localFile.path, + active: true, + }); + }); + + it('sets current active file to not active', () => { + const f = file('newActive'); + store.state.entries[f.path] = f; + localFile.active = true; + store.state.openFiles.push(localFile); + + const commit = jest.fn(); + + actions.setFileActive( + { commit, state: store.state, getters: store.getters, dispatch() {} }, + f.path, + ); + + expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', { + path: localFile.path, + active: false, + }); + }); + }); + + describe('getFileData', () => { + let localFile; + + beforeEach(() => { + jest.spyOn(service, 'getFileData'); + + localFile = file(`newCreate-${Math.random()}`); + store.state.entries[localFile.path] = localFile; + + store.state.currentProjectId = 'test/test'; + store.state.currentBranchId = 'master'; + + store.state.projects['test/test'] = { + branches: { + master: { + commit: { + id: '7297abc', + }, + }, + }, + }; + }); + + describe('success', () => { + beforeEach(() => { + mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`).replyOnce( + 200, + { + blame_path: 'blame_path', + commits_path: 'commits_path', + permalink: 'permalink', + raw_path: 'raw_path', + binary: false, + html: '123', + render_error: '', + }, + { + 'page-title': 'testing getFileData', + }, + ); + }); + + it('calls the service', done => { + store + .dispatch('getFileData', { path: localFile.path }) + .then(() => { + expect(service.getFileData).toHaveBeenCalledWith( + `${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`, + ); + + done(); + }) + .catch(done.fail); + }); + + it('sets the file data', done => { + store + .dispatch('getFileData', { path: localFile.path }) + .then(() => { + expect(localFile.blamePath).toBe('blame_path'); + + done(); + }) + .catch(done.fail); + }); + + it('sets document title with the branchId', done => { + store + .dispatch('getFileData', { path: localFile.path }) + .then(() => { + expect(document.title).toBe(`${localFile.path} · master · test/test · GitLab`); + done(); + }) + .catch(done.fail); + }); + + it('sets the file as active', done => { + store + .dispatch('getFileData', { path: localFile.path }) + .then(() => { + expect(localFile.active).toBeTruthy(); + + done(); + }) + .catch(done.fail); + }); + + it('sets the file not as active if we pass makeFileActive false', done => { + store + .dispatch('getFileData', { path: localFile.path, makeFileActive: false }) + .then(() => { + expect(localFile.active).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + + it('adds the file to open files', done => { + store + .dispatch('getFileData', { path: localFile.path }) + .then(() => { + expect(store.state.openFiles.length).toBe(1); + expect(store.state.openFiles[0].name).toBe(localFile.name); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('Re-named success', () => { + beforeEach(() => { + localFile = file(`newCreate-${Math.random()}`); + localFile.url = `project/getFileDataURL`; + localFile.prevPath = 'old-dull-file'; + localFile.path = 'new-shiny-file'; + store.state.entries[localFile.path] = localFile; + + mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/old-dull-file`).replyOnce( + 200, + { + blame_path: 'blame_path', + commits_path: 'commits_path', + permalink: 'permalink', + raw_path: 'raw_path', + binary: false, + html: '123', + render_error: '', + }, + { + 'page-title': 'testing old-dull-file', + }, + ); + }); + + it('sets document title considering `prevPath` on a file', done => { + store + .dispatch('getFileData', { path: localFile.path }) + .then(() => { + expect(document.title).toBe(`new-shiny-file · master · test/test · GitLab`); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`).networkError(); + }); + + it('dispatches error action', done => { + const dispatch = jest.fn(); + + actions + .getFileData( + { state: store.state, commit() {}, dispatch, getters: store.getters }, + { path: localFile.path }, + ) + .then(() => { + expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { + text: 'An error occurred whilst loading the file.', + action: expect.any(Function), + actionText: 'Please try again', + actionPayload: { + path: localFile.path, + makeFileActive: true, + }, + }); + + done(); + }) + .catch(done.fail); + }); + }); + }); + + describe('getRawFileData', () => { + let tmpFile; + + beforeEach(() => { + jest.spyOn(service, 'getRawFileData'); + + tmpFile = file('tmpFile'); + store.state.entries[tmpFile.path] = tmpFile; + }); + + describe('success', () => { + beforeEach(() => { + mock.onGet(/(.*)/).replyOnce(200, 'raw'); + }); + + it('calls getRawFileData service method', done => { + store + .dispatch('getRawFileData', { path: tmpFile.path }) + .then(() => { + expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); + + done(); + }) + .catch(done.fail); + }); + + it('updates file raw data', done => { + store + .dispatch('getRawFileData', { path: tmpFile.path }) + .then(() => { + expect(tmpFile.raw).toBe('raw'); + + done(); + }) + .catch(done.fail); + }); + + it('calls also getBaseRawFileData service method', done => { + jest.spyOn(service, 'getBaseRawFileData').mockReturnValue(Promise.resolve('baseraw')); + + store.state.currentProjectId = 'gitlab-org/gitlab-ce'; + store.state.currentMergeRequestId = '1'; + store.state.projects = { + 'gitlab-org/gitlab-ce': { + mergeRequests: { + 1: { + baseCommitSha: 'SHA', + }, + }, + }, + }; + + tmpFile.mrChange = { new_file: false }; + + store + .dispatch('getRawFileData', { path: tmpFile.path }) + .then(() => { + expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA'); + expect(tmpFile.baseRaw).toBe('baseraw'); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('return JSON', () => { + beforeEach(() => { + mock.onGet(/(.*)/).replyOnce(200, JSON.stringify({ test: '123' })); + }); + + it('does not parse returned JSON', done => { + store + .dispatch('getRawFileData', { path: tmpFile.path }) + .then(() => { + expect(tmpFile.raw).toEqual('{"test":"123"}'); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(/(.*)/).networkError(); + }); + + it('dispatches error action', done => { + const dispatch = jest.fn(); + + actions + .getRawFileData({ state: store.state, commit() {}, dispatch }, { path: tmpFile.path }) + .then(done.fail) + .catch(() => { + expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { + text: 'An error occurred whilst loading the file content.', + action: expect.any(Function), + actionText: 'Please try again', + actionPayload: { + path: tmpFile.path, + }, + }); + + done(); + }); + }); + }); + }); + + describe('changeFileContent', () => { + let tmpFile; + + beforeEach(() => { + tmpFile = file('tmpFile'); + tmpFile.content = '\n'; + tmpFile.raw = '\n'; + store.state.entries[tmpFile.path] = tmpFile; + }); + + it('updates file content', done => { + store + .dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content\n', + }) + .then(() => { + expect(tmpFile.content).toBe('content\n'); + + done(); + }) + .catch(done.fail); + }); + + it('adds a newline to the end of the file if it doesnt already exist', done => { + store + .dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content', + }) + .then(() => { + expect(tmpFile.content).toBe('content\n'); + + done(); + }) + .catch(done.fail); + }); + + it('adds file into changedFiles array', done => { + store + .dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content', + }) + .then(() => { + expect(store.state.changedFiles.length).toBe(1); + + done(); + }) + .catch(done.fail); + }); + + it('adds file once into changedFiles array', done => { + store + .dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content', + }) + .then(() => + store.dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content 123', + }), + ) + .then(() => { + expect(store.state.changedFiles.length).toBe(1); + + done(); + }) + .catch(done.fail); + }); + + it('removes file from changedFiles array if not changed', done => { + store + .dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content\n', + }) + .then(() => + store.dispatch('changeFileContent', { + path: tmpFile.path, + content: '\n', + }), + ) + .then(() => { + expect(store.state.changedFiles.length).toBe(0); + + done(); + }) + .catch(done.fail); + }); + + it('bursts unused seal', done => { + store + .dispatch('changeFileContent', { + path: tmpFile.path, + content: 'content', + }) + .then(() => { + expect(store.state.unusedSeal).toBe(false); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('discardFileChanges', () => { + let tmpFile; + + beforeEach(() => { + jest.spyOn(eventHub, '$on').mockImplementation(() => {}); + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + tmpFile = file(); + tmpFile.content = 'testing'; + + store.state.changedFiles.push(tmpFile); + store.state.entries[tmpFile.path] = tmpFile; + }); + + it('resets file content', done => { + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(tmpFile.content).not.toBe('testing'); + + done(); + }) + .catch(done.fail); + }); + + it('removes file from changedFiles array', done => { + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(store.state.changedFiles.length).toBe(0); + + done(); + }) + .catch(done.fail); + }); + + it('closes temp file', done => { + tmpFile.tempFile = true; + tmpFile.opened = true; + + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(tmpFile.opened).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + + it('does not re-open a closed temp file', done => { + tmpFile.tempFile = true; + + expect(tmpFile.opened).toBeFalsy(); + + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(tmpFile.opened).toBeFalsy(); + + done(); + }) + .catch(done.fail); + }); + + it('pushes route for active file', done => { + tmpFile.active = true; + store.state.openFiles.push(tmpFile); + + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(router.push).toHaveBeenCalledWith(`/project${tmpFile.url}`); + + done(); + }) + .catch(done.fail); + }); + + it('emits eventHub event to dispose cached model', done => { + store + .dispatch('discardFileChanges', tmpFile.path) + .then(() => { + expect(eventHub.$emit).toHaveBeenCalled(); + + done(); + }) + .catch(done.fail); + }); + }); + + describe('stageChange', () => { + it('calls STAGE_CHANGE with file path', done => { + testAction( + actions.stageChange, + 'path', + store.state, + [ + { type: types.STAGE_CHANGE, payload: 'path' }, + { type: types.SET_LAST_COMMIT_MSG, payload: '' }, + ], + [], + done, + ); + }); + }); + + describe('unstageChange', () => { + it('calls UNSTAGE_CHANGE with file path', done => { + testAction( + actions.unstageChange, + 'path', + store.state, + [{ type: types.UNSTAGE_CHANGE, payload: 'path' }], + [], + done, + ); + }); + }); + + describe('openPendingTab', () => { + let f; + + beforeEach(() => { + f = { + ...file(), + projectId: '123', + }; + + store.state.entries[f.path] = f; + }); + + it('makes file pending in openFiles', done => { + store + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) + .then(() => { + expect(store.state.openFiles[0].pending).toBe(true); + }) + .then(done) + .catch(done.fail); + }); + + it('returns true when opened', done => { + store + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) + .then(added => { + expect(added).toBe(true); + }) + .then(done) + .catch(done.fail); + }); + + it('returns false when already opened', done => { + store.state.openFiles.push({ + ...f, + active: true, + key: `pending-${f.key}`, + }); + + store + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) + .then(added => { + expect(added).toBe(false); + }) + .then(done) + .catch(done.fail); + }); + + it('pushes router URL when added', done => { + store.state.currentBranchId = 'master'; + + store + .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) + .then(() => { + expect(router.push).toHaveBeenCalledWith('/project/123/tree/master/'); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('removePendingTab', () => { + let f; + + beforeEach(() => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + f = { + ...file('pendingFile'), + pending: true, + }; + }); + + it('removes pending file from open files', done => { + store.state.openFiles.push(f); + + store + .dispatch('removePendingTab', f) + .then(() => { + expect(store.state.openFiles.length).toBe(0); + }) + .then(done) + .catch(done.fail); + }); + + it('emits event to dispose model', done => { + store + .dispatch('removePendingTab', f) + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.${f.key}`); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('triggerFilesChange', () => { + beforeEach(() => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + }); + + it('emits event that files have changed', done => { + store + .dispatch('triggerFilesChange') + .then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('ide.files.change'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/frontend/image_diff/helpers/init_image_diff_spec.js b/spec/frontend/image_diff/helpers/init_image_diff_spec.js new file mode 100644 index 00000000000..dc872ace265 --- /dev/null +++ b/spec/frontend/image_diff/helpers/init_image_diff_spec.js @@ -0,0 +1,52 @@ +import initImageDiffHelper from '~/image_diff/helpers/init_image_diff'; +import ImageDiff from '~/image_diff/image_diff'; +import ReplacedImageDiff from '~/image_diff/replaced_image_diff'; + +describe('initImageDiff', () => { + let glCache; + let fileEl; + + beforeEach(() => { + window.gl = window.gl || (window.gl = {}); + glCache = window.gl; + fileEl = document.createElement('div'); + fileEl.innerHTML = ` +
+ `; + + jest.spyOn(ReplacedImageDiff.prototype, 'init').mockImplementation(() => {}); + jest.spyOn(ImageDiff.prototype, 'init').mockImplementation(() => {}); + }); + + afterEach(() => { + window.gl = glCache; + }); + + it('should initialize ImageDiff if js-single-image', () => { + const diffFileEl = fileEl.querySelector('.diff-file'); + diffFileEl.innerHTML = ` +
+
+ `; + + const imageDiff = initImageDiffHelper.initImageDiff(fileEl, true, false); + + expect(ImageDiff.prototype.init).toHaveBeenCalled(); + expect(imageDiff.canCreateNote).toEqual(true); + expect(imageDiff.renderCommentBadge).toEqual(false); + }); + + it('should initialize ReplacedImageDiff if js-replaced-image', () => { + const diffFileEl = fileEl.querySelector('.diff-file'); + diffFileEl.innerHTML = ` +
+
+ `; + + const replacedImageDiff = initImageDiffHelper.initImageDiff(fileEl, false, true); + + expect(ReplacedImageDiff.prototype.init).toHaveBeenCalled(); + expect(replacedImageDiff.canCreateNote).toEqual(false); + expect(replacedImageDiff.renderCommentBadge).toEqual(true); + }); +}); diff --git a/spec/frontend/image_diff/init_discussion_tab_spec.js b/spec/frontend/image_diff/init_discussion_tab_spec.js new file mode 100644 index 00000000000..f459fdf5a08 --- /dev/null +++ b/spec/frontend/image_diff/init_discussion_tab_spec.js @@ -0,0 +1,42 @@ +import initDiscussionTab from '~/image_diff/init_discussion_tab'; +import initImageDiffHelper from '~/image_diff/helpers/init_image_diff'; + +describe('initDiscussionTab', () => { + beforeEach(() => { + setFixtures(` +
+
+
+
+ `); + }); + + it('should pass canCreateNote as false to initImageDiff', done => { + jest + .spyOn(initImageDiffHelper, 'initImageDiff') + .mockImplementation((diffFileEl, canCreateNote) => { + expect(canCreateNote).toEqual(false); + done(); + }); + + initDiscussionTab(); + }); + + it('should pass renderCommentBadge as true to initImageDiff', done => { + jest + .spyOn(initImageDiffHelper, 'initImageDiff') + .mockImplementation((diffFileEl, canCreateNote, renderCommentBadge) => { + expect(renderCommentBadge).toEqual(true); + done(); + }); + + initDiscussionTab(); + }); + + it('should call initImageDiff for each diffFileEls', () => { + jest.spyOn(initImageDiffHelper, 'initImageDiff').mockImplementation(() => {}); + initDiscussionTab(); + + expect(initImageDiffHelper.initImageDiff.mock.calls.length).toEqual(2); + }); +}); diff --git a/spec/frontend/issue_show/components/edit_actions_spec.js b/spec/frontend/issue_show/components/edit_actions_spec.js new file mode 100644 index 00000000000..b0c1894058e --- /dev/null +++ b/spec/frontend/issue_show/components/edit_actions_spec.js @@ -0,0 +1,134 @@ +import Vue from 'vue'; +import editActions from '~/issue_show/components/edit_actions.vue'; +import eventHub from '~/issue_show/event_hub'; +import Store from '~/issue_show/stores'; + +describe('Edit Actions components', () => { + let vm; + + beforeEach(done => { + const Component = Vue.extend(editActions); + const store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); + store.formState.title = 'test'; + + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + vm = new Component({ + propsData: { + canDestroy: true, + formState: store.formState, + issuableType: 'issue', + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('renders all buttons as enabled', () => { + expect(vm.$el.querySelectorAll('.disabled').length).toBe(0); + + expect(vm.$el.querySelectorAll('[disabled]').length).toBe(0); + }); + + it('does not render delete button if canUpdate is false', done => { + vm.canDestroy = false; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn-danger')).toBeNull(); + + done(); + }); + }); + + it('disables submit button when title is blank', done => { + vm.formState.title = ''; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn-success').getAttribute('disabled')).toBe('disabled'); + + done(); + }); + }); + + it('should not show delete button if showDeleteButton is false', done => { + vm.showDeleteButton = false; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn-danger')).toBeNull(); + done(); + }); + }); + + describe('updateIssuable', () => { + it('sends update.issauble event when clicking save button', () => { + vm.$el.querySelector('.btn-success').click(); + + expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable'); + }); + + it('shows loading icon after clicking save button', done => { + vm.$el.querySelector('.btn-success').click(); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn-success .fa')).not.toBeNull(); + + done(); + }); + }); + + it('disabled button after clicking save button', done => { + vm.$el.querySelector('.btn-success').click(); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn-success').getAttribute('disabled')).toBe('disabled'); + + done(); + }); + }); + }); + + describe('closeForm', () => { + it('emits close.form when clicking cancel', () => { + vm.$el.querySelector('.btn-default').click(); + + expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); + }); + }); + + describe('deleteIssuable', () => { + it('sends delete.issuable event when clicking save button', () => { + jest.spyOn(window, 'confirm').mockReturnValue(true); + vm.$el.querySelector('.btn-danger').click(); + + expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable', { destroy_confirm: true }); + }); + + it('shows loading icon after clicking delete button', done => { + jest.spyOn(window, 'confirm').mockReturnValue(true); + vm.$el.querySelector('.btn-danger').click(); + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.btn-danger .fa')).not.toBeNull(); + + done(); + }); + }); + + it('does no actions when confirm is false', done => { + jest.spyOn(window, 'confirm').mockReturnValue(false); + vm.$el.querySelector('.btn-danger').click(); + + Vue.nextTick(() => { + expect(eventHub.$emit).not.toHaveBeenCalledWith('delete.issuable'); + + expect(vm.$el.querySelector('.btn-danger .fa')).toBeNull(); + + done(); + }); + }); + }); +}); diff --git a/spec/frontend/issue_show/components/fields/description_spec.js b/spec/frontend/issue_show/components/fields/description_spec.js new file mode 100644 index 00000000000..8ea326ad1ee --- /dev/null +++ b/spec/frontend/issue_show/components/fields/description_spec.js @@ -0,0 +1,70 @@ +import Vue from 'vue'; +import eventHub from '~/issue_show/event_hub'; +import Store from '~/issue_show/stores'; +import descriptionField from '~/issue_show/components/fields/description.vue'; +import { keyboardDownEvent } from '../../helpers'; + +describe('Description field component', () => { + let vm; + let store; + + beforeEach(done => { + const Component = Vue.extend(descriptionField); + const el = document.createElement('div'); + store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); + store.formState.description = 'test'; + + document.body.appendChild(el); + + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + vm = new Component({ + el, + propsData: { + markdownPreviewPath: '/', + markdownDocsPath: '/', + formState: store.formState, + }, + }).$mount(); + + Vue.nextTick(done); + }); + + it('renders markdown field with description', () => { + expect(vm.$el.querySelector('.md-area textarea').value).toBe('test'); + }); + + it('renders markdown field with a markdown description', done => { + store.formState.description = '**test**'; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.md-area textarea').value).toBe('**test**'); + + done(); + }); + }); + + it('focuses field when mounted', () => { + expect(document.activeElement).toBe(vm.$refs.textarea); + }); + + it('triggers update with meta+enter', () => { + vm.$el.querySelector('.md-area textarea').dispatchEvent(keyboardDownEvent(13, true)); + + expect(eventHub.$emit).toHaveBeenCalled(); + }); + + it('triggers update with ctrl+enter', () => { + vm.$el.querySelector('.md-area textarea').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect(eventHub.$emit).toHaveBeenCalled(); + }); + + it('has a ref named `textarea`', () => { + expect(vm.$refs.textarea).not.toBeNull(); + }); +}); diff --git a/spec/frontend/issue_show/components/fields/title_spec.js b/spec/frontend/issue_show/components/fields/title_spec.js new file mode 100644 index 00000000000..99e8658b89f --- /dev/null +++ b/spec/frontend/issue_show/components/fields/title_spec.js @@ -0,0 +1,48 @@ +import Vue from 'vue'; +import eventHub from '~/issue_show/event_hub'; +import Store from '~/issue_show/stores'; +import titleField from '~/issue_show/components/fields/title.vue'; +import { keyboardDownEvent } from '../../helpers'; + +describe('Title field component', () => { + let vm; + let store; + + beforeEach(() => { + const Component = Vue.extend(titleField); + store = new Store({ + titleHtml: '', + descriptionHtml: '', + issuableRef: '', + }); + store.formState.title = 'test'; + + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + vm = new Component({ + propsData: { + formState: store.formState, + }, + }).$mount(); + }); + + it('renders form control with formState title', () => { + expect(vm.$el.querySelector('.form-control').value).toBe('test'); + }); + + it('triggers update with meta+enter', () => { + vm.$el.querySelector('.form-control').dispatchEvent(keyboardDownEvent(13, true)); + + expect(eventHub.$emit).toHaveBeenCalled(); + }); + + it('triggers update with ctrl+enter', () => { + vm.$el.querySelector('.form-control').dispatchEvent(keyboardDownEvent(13, false, true)); + + expect(eventHub.$emit).toHaveBeenCalled(); + }); + + it('has a ref named `input`', () => { + expect(vm.$refs.input).not.toBeNull(); + }); +}); diff --git a/spec/frontend/issue_show/index_spec.js b/spec/frontend/issue_show/index_spec.js new file mode 100644 index 00000000000..e80d1b83c11 --- /dev/null +++ b/spec/frontend/issue_show/index_spec.js @@ -0,0 +1,19 @@ +import initIssueableApp from '~/issue_show'; + +describe('Issue show index', () => { + describe('initIssueableApp', () => { + it('should initialize app with no potential XSS attack', () => { + const d = document.createElement('div'); + d.id = 'js-issuable-app-initial-data'; + d.innerHTML = JSON.stringify({ + initialDescriptionHtml: '<img src=x onerror=alert(1)>', + }); + document.body.appendChild(d); + + const alertSpy = jest.spyOn(window, 'alert'); + initIssueableApp(); + + expect(alertSpy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job_log_controllers_spec.js new file mode 100644 index 00000000000..04f20811601 --- /dev/null +++ b/spec/frontend/jobs/components/job_log_controllers_spec.js @@ -0,0 +1,208 @@ +import Vue from 'vue'; +import component from '~/jobs/components/job_log_controllers.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Job log controllers', () => { + const Component = Vue.extend(component); + let vm; + + afterEach(() => { + vm.$destroy(); + }); + + const props = { + rawPath: '/raw', + erasePath: '/erase', + size: 511952, + isScrollTopDisabled: false, + isScrollBottomDisabled: false, + isScrollingDown: true, + isTraceSizeVisible: true, + }; + + describe('Truncate information', () => { + describe('with isTraceSizeVisible', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + it('renders size information', () => { + expect(vm.$el.querySelector('.js-truncated-info').textContent).toContain('499.95 KiB'); + }); + + it('renders link to raw trace', () => { + expect(vm.$el.querySelector('.js-raw-link').getAttribute('href')).toEqual('/raw'); + }); + }); + }); + + describe('links section', () => { + describe('with raw trace path', () => { + it('renders raw trace link', () => { + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector('.js-raw-link-controller').getAttribute('href')).toEqual( + '/raw', + ); + }); + }); + + describe('without raw trace path', () => { + it('does not render raw trace link', () => { + vm = mountComponent(Component, { + erasePath: '/erase', + size: 511952, + isScrollTopDisabled: true, + isScrollBottomDisabled: true, + isScrollingDown: false, + isTraceSizeVisible: true, + }); + + expect(vm.$el.querySelector('.js-raw-link-controller')).toBeNull(); + }); + }); + + describe('when is erasable', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + it('renders erase job link', () => { + expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull(); + }); + }); + + describe('when it is not erasable', () => { + it('does not render erase button', () => { + vm = mountComponent(Component, { + rawPath: '/raw', + size: 511952, + isScrollTopDisabled: true, + isScrollBottomDisabled: true, + isScrollingDown: false, + isTraceSizeVisible: true, + }); + + expect(vm.$el.querySelector('.js-erase-link')).toBeNull(); + }); + }); + }); + + describe('scroll buttons', () => { + describe('scroll top button', () => { + describe('when user can scroll top', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + it('renders enabled scroll top button', () => { + expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toBeNull(); + }); + + it('emits scrollJobLogTop event on click', () => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + vm.$el.querySelector('.js-scroll-top').click(); + + expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogTop'); + }); + }); + + describe('when user can not scroll top', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawPath: '/raw', + erasePath: '/erase', + size: 511952, + isScrollTopDisabled: true, + isScrollBottomDisabled: false, + isScrollingDown: false, + isTraceSizeVisible: true, + }); + }); + + it('renders disabled scroll top button', () => { + expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toEqual( + 'disabled', + ); + }); + + it('does not emit scrollJobLogTop event on click', () => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + vm.$el.querySelector('.js-scroll-top').click(); + + expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogTop'); + }); + }); + }); + + describe('scroll bottom button', () => { + describe('when user can scroll bottom', () => { + beforeEach(() => { + vm = mountComponent(Component, props); + }); + + it('renders enabled scroll bottom button', () => { + expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toBeNull(); + }); + + it('emits scrollJobLogBottom event on click', () => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + vm.$el.querySelector('.js-scroll-bottom').click(); + + expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogBottom'); + }); + }); + + describe('when user can not scroll bottom', () => { + beforeEach(() => { + vm = mountComponent(Component, { + rawPath: '/raw', + erasePath: '/erase', + size: 511952, + isScrollTopDisabled: false, + isScrollBottomDisabled: true, + isScrollingDown: false, + isTraceSizeVisible: true, + }); + }); + + it('renders disabled scroll bottom button', () => { + expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toEqual( + 'disabled', + ); + }); + + it('does not emit scrollJobLogBottom event on click', () => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + vm.$el.querySelector('.js-scroll-bottom').click(); + + expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogBottom'); + }); + }); + + describe('while isScrollingDown is true', () => { + it('renders animate class for the scroll down button', () => { + vm = mountComponent(Component, props); + + expect(vm.$el.querySelector('.js-scroll-bottom').className).toContain('animate'); + }); + }); + + describe('while isScrollingDown is false', () => { + it('does not render animate class for the scroll down button', () => { + vm = mountComponent(Component, { + rawPath: '/raw', + erasePath: '/erase', + size: 511952, + isScrollTopDisabled: true, + isScrollBottomDisabled: false, + isScrollingDown: false, + isTraceSizeVisible: true, + }); + + expect(vm.$el.querySelector('.js-scroll-bottom').className).not.toContain('animate'); + }); + }); + }); + }); +}); diff --git a/spec/frontend/namespace_select_spec.js b/spec/frontend/namespace_select_spec.js new file mode 100644 index 00000000000..399fa950769 --- /dev/null +++ b/spec/frontend/namespace_select_spec.js @@ -0,0 +1,66 @@ +import $ from 'jquery'; +import NamespaceSelect from '~/namespace_select'; + +describe('NamespaceSelect', () => { + beforeEach(() => { + jest.spyOn($.fn, 'glDropdown').mockImplementation(() => {}); + }); + + it('initializes glDropdown', () => { + const dropdown = document.createElement('div'); + + // eslint-disable-next-line no-new + new NamespaceSelect({ dropdown }); + + expect($.fn.glDropdown).toHaveBeenCalled(); + }); + + describe('as input', () => { + let glDropdownOptions; + + beforeEach(() => { + const dropdown = document.createElement('div'); + // eslint-disable-next-line no-new + new NamespaceSelect({ dropdown }); + [[glDropdownOptions]] = $.fn.glDropdown.mock.calls; + }); + + it('prevents click events', () => { + const dummyEvent = new Event('dummy'); + jest.spyOn(dummyEvent, 'preventDefault').mockImplementation(() => {}); + + glDropdownOptions.clicked({ e: dummyEvent }); + + expect(dummyEvent.preventDefault).toHaveBeenCalled(); + }); + }); + + describe('as filter', () => { + let glDropdownOptions; + + beforeEach(() => { + const dropdown = document.createElement('div'); + dropdown.dataset.isFilter = 'true'; + // eslint-disable-next-line no-new + new NamespaceSelect({ dropdown }); + [[glDropdownOptions]] = $.fn.glDropdown.mock.calls; + }); + + it('does not prevent click events', () => { + const dummyEvent = new Event('dummy'); + jest.spyOn(dummyEvent, 'preventDefault').mockImplementation(() => {}); + + glDropdownOptions.clicked({ e: dummyEvent }); + + expect(dummyEvent.preventDefault).not.toHaveBeenCalled(); + }); + + it('sets URL of dropdown items', () => { + const dummyNamespace = { id: 'eal' }; + + const itemUrl = glDropdownOptions.url(dummyNamespace); + + expect(itemUrl).toContain(`namespace_id=${dummyNamespace.id}`); + }); + }); +}); diff --git a/spec/frontend/new_branch_spec.js b/spec/frontend/new_branch_spec.js new file mode 100644 index 00000000000..cff7ec1a9ee --- /dev/null +++ b/spec/frontend/new_branch_spec.js @@ -0,0 +1,203 @@ +import $ from 'jquery'; +import NewBranchForm from '~/new_branch_form'; + +describe('Branch', () => { + let testContext; + + beforeEach(() => { + testContext = {}; + }); + + describe('create a new branch', () => { + preloadFixtures('branches/new_branch.html'); + + function fillNameWith(value) { + $('.js-branch-name') + .val(value) + .trigger('blur'); + } + + function expectToHaveError(error) { + expect($('.js-branch-name-error span').text()).toEqual(error); + } + + beforeEach(() => { + loadFixtures('branches/new_branch.html'); + $('form').on('submit', e => e.preventDefault()); + testContext.form = new NewBranchForm($('.js-create-branch-form'), []); + }); + + it("can't start with a dot", () => { + fillNameWith('.foo'); + expectToHaveError("can't start with '.'"); + }); + + it("can't start with a slash", () => { + fillNameWith('/foo'); + expectToHaveError("can't start with '/'"); + }); + + it("can't have two consecutive dots", () => { + fillNameWith('foo..bar'); + expectToHaveError("can't contain '..'"); + }); + + it("can't have spaces anywhere", () => { + fillNameWith(' foo'); + expectToHaveError("can't contain spaces"); + fillNameWith('foo bar'); + expectToHaveError("can't contain spaces"); + fillNameWith('foo '); + expectToHaveError("can't contain spaces"); + }); + + it("can't have ~ anywhere", () => { + fillNameWith('~foo'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~bar'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~'); + expectToHaveError("can't contain '~'"); + }); + + it("can't have tilde anwhere", () => { + fillNameWith('~foo'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~bar'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~'); + expectToHaveError("can't contain '~'"); + }); + + it("can't have caret anywhere", () => { + fillNameWith('^foo'); + expectToHaveError("can't contain '^'"); + fillNameWith('foo^bar'); + expectToHaveError("can't contain '^'"); + fillNameWith('foo^'); + expectToHaveError("can't contain '^'"); + }); + + it("can't have : anywhere", () => { + fillNameWith(':foo'); + expectToHaveError("can't contain ':'"); + fillNameWith('foo:bar'); + expectToHaveError("can't contain ':'"); + fillNameWith(':foo'); + expectToHaveError("can't contain ':'"); + }); + + it("can't have question mark anywhere", () => { + fillNameWith('?foo'); + expectToHaveError("can't contain '?'"); + fillNameWith('foo?bar'); + expectToHaveError("can't contain '?'"); + fillNameWith('foo?'); + expectToHaveError("can't contain '?'"); + }); + + it("can't have asterisk anywhere", () => { + fillNameWith('*foo'); + expectToHaveError("can't contain '*'"); + fillNameWith('foo*bar'); + expectToHaveError("can't contain '*'"); + fillNameWith('foo*'); + expectToHaveError("can't contain '*'"); + }); + + it("can't have open bracket anywhere", () => { + fillNameWith('[foo'); + expectToHaveError("can't contain '['"); + fillNameWith('foo[bar'); + expectToHaveError("can't contain '['"); + fillNameWith('foo['); + expectToHaveError("can't contain '['"); + }); + + it("can't have a backslash anywhere", () => { + fillNameWith('\\foo'); + expectToHaveError("can't contain '\\'"); + fillNameWith('foo\\bar'); + expectToHaveError("can't contain '\\'"); + fillNameWith('foo\\'); + expectToHaveError("can't contain '\\'"); + }); + + it("can't contain a sequence @{ anywhere", () => { + fillNameWith('@{foo'); + expectToHaveError("can't contain '@{'"); + fillNameWith('foo@{bar'); + expectToHaveError("can't contain '@{'"); + fillNameWith('foo@{'); + expectToHaveError("can't contain '@{'"); + }); + + it("can't have consecutive slashes", () => { + fillNameWith('foo//bar'); + expectToHaveError("can't contain consecutive slashes"); + }); + + it("can't end with a slash", () => { + fillNameWith('foo/'); + expectToHaveError("can't end in '/'"); + }); + + it("can't end with a dot", () => { + fillNameWith('foo.'); + expectToHaveError("can't end in '.'"); + }); + + it("can't end with .lock", () => { + fillNameWith('foo.lock'); + expectToHaveError("can't end in '.lock'"); + }); + + it("can't be the single character @", () => { + fillNameWith('@'); + expectToHaveError("can't be '@'"); + }); + + it('concatenates all error messages', () => { + fillNameWith('/foo bar?~.'); + expectToHaveError("can't start with '/', can't contain spaces, '?', '~', can't end in '.'"); + }); + + it("doesn't duplicate error messages", () => { + fillNameWith('?foo?bar?zoo?'); + expectToHaveError("can't contain '?'"); + }); + + it('removes the error message when is a valid name', () => { + fillNameWith('foo?bar'); + + expect($('.js-branch-name-error span').length).toEqual(1); + fillNameWith('foobar'); + + expect($('.js-branch-name-error span').length).toEqual(0); + }); + + it('can have dashes anywhere', () => { + fillNameWith('-foo-bar-zoo-'); + + expect($('.js-branch-name-error span').length).toEqual(0); + }); + + it('can have underscores anywhere', () => { + fillNameWith('_foo_bar_zoo_'); + + expect($('.js-branch-name-error span').length).toEqual(0); + }); + + it('can have numbers anywhere', () => { + fillNameWith('1foo2bar3zoo4'); + + expect($('.js-branch-name-error span').length).toEqual(0); + }); + + it('can be only letters', () => { + fillNameWith('foo'); + + expect($('.js-branch-name-error span').length).toEqual(0); + }); + }); +}); diff --git a/spec/frontend/notes/components/discussion_filter_note_spec.js b/spec/frontend/notes/components/discussion_filter_note_spec.js new file mode 100644 index 00000000000..6b5f42a84e8 --- /dev/null +++ b/spec/frontend/notes/components/discussion_filter_note_spec.js @@ -0,0 +1,93 @@ +import Vue from 'vue'; +import DiscussionFilterNote from '~/notes/components/discussion_filter_note.vue'; +import eventHub from '~/notes/event_hub'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('DiscussionFilterNote component', () => { + let vm; + + const createComponent = () => { + const Component = Vue.extend(DiscussionFilterNote); + + return mountComponent(Component); + }; + + beforeEach(() => { + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('computed', () => { + describe('timelineContent', () => { + it('returns string containing instruction for switching feed type', () => { + expect(vm.timelineContent).toBe( + "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", + ); + }); + }); + }); + + describe('methods', () => { + describe('selectFilter', () => { + it('emits `dropdownSelect` event on `eventHub` with provided param', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + vm.selectFilter(1); + + expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1); + }); + }); + }); + + describe('template', () => { + it('renders component container element', () => { + expect(vm.$el.classList.contains('discussion-filter-note')).toBe(true); + }); + + it('renders comment icon element', () => { + expect(vm.$el.querySelector('.timeline-icon svg use').getAttribute('xlink:href')).toContain( + 'comment', + ); + }); + + it('renders filter information note', () => { + expect(vm.$el.querySelector('.timeline-content').innerText.trim()).toContain( + "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", + ); + }); + + it('renders filter buttons', () => { + const buttonsContainerEl = vm.$el.querySelector('.discussion-filter-actions'); + + expect(buttonsContainerEl.querySelector('button:first-child').innerText.trim()).toContain( + 'Show all activity', + ); + + expect(buttonsContainerEl.querySelector('button:last-child').innerText.trim()).toContain( + 'Show comments only', + ); + }); + + it('clicking `Show all activity` button calls `selectFilter("all")` method', () => { + const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:first-child'); + jest.spyOn(vm, 'selectFilter').mockImplementation(() => {}); + + showAllBtn.dispatchEvent(new Event('click')); + + expect(vm.selectFilter).toHaveBeenCalledWith(0); + }); + + it('clicking `Show comments only` button calls `selectFilter("comments")` method', () => { + const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:last-child'); + jest.spyOn(vm, 'selectFilter').mockImplementation(() => {}); + + showAllBtn.dispatchEvent(new Event('click')); + + expect(vm.selectFilter).toHaveBeenCalledWith(1); + }); + }); +}); diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js new file mode 100644 index 00000000000..9b432387654 --- /dev/null +++ b/spec/frontend/notes/components/note_header_spec.js @@ -0,0 +1,125 @@ +import Vue from 'vue'; +import noteHeader from '~/notes/components/note_header.vue'; +import createStore from '~/notes/stores'; + +describe('note_header component', () => { + let store; + let vm; + let Component; + + beforeEach(() => { + Component = Vue.extend(noteHeader); + store = createStore(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('individual note', () => { + beforeEach(() => { + vm = new Component({ + store, + propsData: { + actionText: 'commented', + actionTextHtml: '', + author: { + avatar_url: null, + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + createdAt: '2017-08-02T10:51:58.559Z', + includeToggle: false, + noteId: '1394', + expanded: true, + }, + }).$mount(); + }); + + it('should render user information', () => { + expect(vm.$el.querySelector('.note-header-author-name').textContent.trim()).toEqual('Root'); + expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual('/root'); + expect(vm.$el.querySelector('.note-header-info a').dataset.userId).toEqual('1'); + expect(vm.$el.querySelector('.note-header-info a').dataset.username).toEqual('root'); + expect(vm.$el.querySelector('.note-header-info a').classList).toContain('js-user-link'); + }); + + it('should render timestamp link', () => { + expect(vm.$el.querySelector('a[href="#note_1394"]')).toBeDefined(); + }); + + it('should not render user information when prop `author` is empty object', done => { + vm.author = {}; + Vue.nextTick() + .then(() => { + expect(vm.$el.querySelector('.note-header-author-name')).toBeNull(); + }) + .then(done) + .catch(done.fail); + }); + }); + + describe('discussion', () => { + beforeEach(() => { + vm = new Component({ + store, + propsData: { + actionText: 'started a discussion', + actionTextHtml: '', + author: { + avatar_url: null, + id: 1, + name: 'Root', + path: '/root', + state: 'active', + username: 'root', + }, + createdAt: '2017-08-02T10:51:58.559Z', + includeToggle: true, + noteId: '1395', + expanded: true, + }, + }).$mount(); + }); + + it('should render toggle button', () => { + expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined(); + }); + + it('emits toggle event on click', done => { + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + + vm.$el.querySelector('.js-vue-toggle-button').click(); + + Vue.nextTick(() => { + expect(vm.$emit).toHaveBeenCalledWith('toggleHandler'); + done(); + }); + }); + + it('renders up arrow when open', done => { + vm.expanded = true; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( + 'fa-chevron-up', + ); + done(); + }); + }); + + it('renders down arrow when closed', done => { + vm.expanded = false; + + Vue.nextTick(() => { + expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( + 'fa-chevron-down', + ); + done(); + }); + }); + }); +}); diff --git a/spec/frontend/notes/stores/getters_spec.js b/spec/frontend/notes/stores/getters_spec.js new file mode 100644 index 00000000000..83417bd70ef --- /dev/null +++ b/spec/frontend/notes/stores/getters_spec.js @@ -0,0 +1,388 @@ +import * as getters from '~/notes/stores/getters'; +import { + notesDataMock, + userDataMock, + noteableDataMock, + individualNote, + collapseNotesMock, + discussion1, + discussion2, + discussion3, + resolvedDiscussion1, + unresolvableDiscussion, +} from '../mock_data'; + +const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; + +// Helper function to ensure that we're using the same schema across tests. +const createDiscussionNeighborParams = (discussionId, diffOrder, step) => ({ + discussionId, + diffOrder, + step, +}); + +describe('Getters Notes Store', () => { + let state; + + preloadFixtures(discussionWithTwoUnresolvedNotes); + + beforeEach(() => { + state = { + discussions: [individualNote], + targetNoteHash: 'hash', + lastFetchedAt: 'timestamp', + isNotesFetched: false, + notesData: notesDataMock, + userData: userDataMock, + noteableData: noteableDataMock, + }; + }); + + describe('showJumpToNextDiscussion', () => { + it('should return true if there are 2 or more unresolved discussions', () => { + const localGetters = { + unresolvedDiscussionsIdsByDate: ['123', '456'], + allResolvableDiscussions: [], + }; + + expect(getters.showJumpToNextDiscussion(state, localGetters)()).toBe(true); + }); + + it('should return false if there are 1 or less unresolved discussions', () => { + const localGetters = { + unresolvedDiscussionsIdsByDate: ['123'], + allResolvableDiscussions: [], + }; + + expect(getters.showJumpToNextDiscussion(state, localGetters)()).toBe(false); + }); + }); + + describe('discussions', () => { + it('should return all discussions in the store', () => { + expect(getters.discussions(state)).toEqual([individualNote]); + }); + }); + + describe('resolvedDiscussionsById', () => { + it('ignores unresolved system notes', () => { + const [discussion] = getJSONFixture(discussionWithTwoUnresolvedNotes); + discussion.notes[0].resolved = true; + discussion.notes[1].resolved = false; + state.discussions.push(discussion); + + expect(getters.resolvedDiscussionsById(state)).toEqual({ + [discussion.id]: discussion, + }); + }); + }); + + describe('Collapsed notes', () => { + const stateCollapsedNotes = { + discussions: collapseNotesMock, + targetNoteHash: 'hash', + lastFetchedAt: 'timestamp', + + notesData: notesDataMock, + userData: userDataMock, + noteableData: noteableDataMock, + }; + + it('should return a single system note when a description was updated multiple times', () => { + expect(getters.discussions(stateCollapsedNotes).length).toEqual(1); + }); + }); + + describe('targetNoteHash', () => { + it('should return `targetNoteHash`', () => { + expect(getters.targetNoteHash(state)).toEqual('hash'); + }); + }); + + describe('getNotesData', () => { + it('should return all data in `notesData`', () => { + expect(getters.getNotesData(state)).toEqual(notesDataMock); + }); + }); + + describe('getNoteableData', () => { + it('should return all data in `noteableData`', () => { + expect(getters.getNoteableData(state)).toEqual(noteableDataMock); + }); + }); + + describe('getUserData', () => { + it('should return all data in `userData`', () => { + expect(getters.getUserData(state)).toEqual(userDataMock); + }); + }); + + describe('notesById', () => { + it('should return the note for the given id', () => { + expect(getters.notesById(state)).toEqual({ 1390: individualNote.notes[0] }); + }); + }); + + describe('getCurrentUserLastNote', () => { + it('should return the last note of the current user', () => { + expect(getters.getCurrentUserLastNote(state)).toEqual(individualNote.notes[0]); + }); + }); + + describe('openState', () => { + it('should return the issue state', () => { + expect(getters.openState(state)).toEqual(noteableDataMock.state); + }); + }); + + describe('isNotesFetched', () => { + it('should return the state for the fetching notes', () => { + expect(getters.isNotesFetched(state)).toBeFalsy(); + }); + }); + + describe('allResolvableDiscussions', () => { + it('should return only resolvable discussions in same order', () => { + state.discussions = [ + discussion3, + unresolvableDiscussion, + discussion1, + unresolvableDiscussion, + discussion2, + ]; + + expect(getters.allResolvableDiscussions(state)).toEqual([ + discussion3, + discussion1, + discussion2, + ]); + }); + + it('should return empty array if there are no resolvable discussions', () => { + state.discussions = [unresolvableDiscussion, unresolvableDiscussion]; + + expect(getters.allResolvableDiscussions(state)).toEqual([]); + }); + }); + + describe('unresolvedDiscussionsIdsByDiff', () => { + it('should return all discussions IDs in diff order', () => { + const localGetters = { + allResolvableDiscussions: [discussion3, discussion1, discussion2], + }; + + expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters)).toEqual([ + 'abc1', + 'abc2', + 'abc3', + ]); + }); + + it('should return empty array if all discussions have been resolved', () => { + const localGetters = { + allResolvableDiscussions: [resolvedDiscussion1], + }; + + expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters)).toEqual([]); + }); + }); + + describe('unresolvedDiscussionsIdsByDate', () => { + it('should return all discussions in date ascending order', () => { + const localGetters = { + allResolvableDiscussions: [discussion3, discussion1, discussion2], + }; + + expect(getters.unresolvedDiscussionsIdsByDate(state, localGetters)).toEqual([ + 'abc2', + 'abc1', + 'abc3', + ]); + }); + + it('should return empty array if all discussions have been resolved', () => { + const localGetters = { + allResolvableDiscussions: [resolvedDiscussion1], + }; + + expect(getters.unresolvedDiscussionsIdsByDate(state, localGetters)).toEqual([]); + }); + }); + + describe('unresolvedDiscussionsIdsOrdered', () => { + const localGetters = { + unresolvedDiscussionsIdsByDate: ['123', '456'], + unresolvedDiscussionsIdsByDiff: ['abc', 'def'], + }; + + it('should return IDs ordered by diff when diffOrder param is true', () => { + expect(getters.unresolvedDiscussionsIdsOrdered(state, localGetters)(true)).toEqual([ + 'abc', + 'def', + ]); + }); + + it('should return IDs ordered by date when diffOrder param is not true', () => { + expect(getters.unresolvedDiscussionsIdsOrdered(state, localGetters)(false)).toEqual([ + '123', + '456', + ]); + + expect(getters.unresolvedDiscussionsIdsOrdered(state, localGetters)(undefined)).toEqual([ + '123', + '456', + ]); + }); + }); + + describe('isLastUnresolvedDiscussion', () => { + const localGetters = { + unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'], + }; + + it('should return true if the discussion id provided is the last', () => { + expect(getters.isLastUnresolvedDiscussion(state, localGetters)('789')).toBe(true); + }); + + it('should return false if the discussion id provided is not the last', () => { + expect(getters.isLastUnresolvedDiscussion(state, localGetters)('123')).toBe(false); + expect(getters.isLastUnresolvedDiscussion(state, localGetters)('456')).toBe(false); + }); + }); + + describe('findUnresolvedDiscussionIdNeighbor', () => { + let localGetters; + beforeEach(() => { + localGetters = { + unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'], + }; + }); + + [ + { step: 1, id: '123', expected: '456' }, + { step: 1, id: '456', expected: '789' }, + { step: 1, id: '789', expected: '123' }, + { step: -1, id: '123', expected: '789' }, + { step: -1, id: '456', expected: '123' }, + { step: -1, id: '789', expected: '456' }, + ].forEach(({ step, id, expected }) => { + it(`with step ${step} and id ${id}, returns next value`, () => { + const params = createDiscussionNeighborParams(id, true, step); + + expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe( + expected, + ); + }); + }); + + describe('with 1 unresolved discussion', () => { + beforeEach(() => { + localGetters = { + unresolvedDiscussionsIdsOrdered: () => ['123'], + }; + }); + + [{ step: 1, id: '123', expected: '123' }, { step: -1, id: '123', expected: '123' }].forEach( + ({ step, id, expected }) => { + it(`with step ${step} and match, returns only value`, () => { + const params = createDiscussionNeighborParams(id, true, step); + + expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe( + expected, + ); + }); + }, + ); + + it('with no match, returns only value', () => { + const params = createDiscussionNeighborParams('bogus', true, 1); + + expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe('123'); + }); + }); + + describe('with 0 unresolved discussions', () => { + beforeEach(() => { + localGetters = { + unresolvedDiscussionsIdsOrdered: () => [], + }; + }); + + [{ step: 1 }, { step: -1 }].forEach(({ step }) => { + it(`with step ${step}, returns undefined`, () => { + const params = createDiscussionNeighborParams('bogus', true, step); + + expect( + getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params), + ).toBeUndefined(); + }); + }); + }); + }); + + describe('findUnresolvedDiscussionIdNeighbor aliases', () => { + let neighbor; + let findUnresolvedDiscussionIdNeighbor; + let localGetters; + + beforeEach(() => { + neighbor = {}; + findUnresolvedDiscussionIdNeighbor = jest.fn(() => neighbor); + localGetters = { findUnresolvedDiscussionIdNeighbor }; + }); + + describe('nextUnresolvedDiscussionId', () => { + it('should return result of find neighbor', () => { + const expectedParams = createDiscussionNeighborParams('123', true, 1); + const result = getters.nextUnresolvedDiscussionId(state, localGetters)('123', true); + + expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams); + expect(result).toBe(neighbor); + }); + }); + + describe('previosuUnresolvedDiscussionId', () => { + it('should return result of find neighbor', () => { + const expectedParams = createDiscussionNeighborParams('123', true, -1); + const result = getters.previousUnresolvedDiscussionId(state, localGetters)('123', true); + + expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams); + expect(result).toBe(neighbor); + }); + }); + }); + + describe('firstUnresolvedDiscussionId', () => { + const localGetters = { + unresolvedDiscussionsIdsByDate: ['123', '456'], + unresolvedDiscussionsIdsByDiff: ['abc', 'def'], + }; + + it('should return the first discussion id by diff when diffOrder param is true', () => { + expect(getters.firstUnresolvedDiscussionId(state, localGetters)(true)).toBe('abc'); + }); + + it('should return the first discussion id by date when diffOrder param is not true', () => { + expect(getters.firstUnresolvedDiscussionId(state, localGetters)(false)).toBe('123'); + expect(getters.firstUnresolvedDiscussionId(state, localGetters)(undefined)).toBe('123'); + }); + + it('should be falsy if all discussions are resolved', () => { + const localGettersFalsy = { + unresolvedDiscussionsIdsByDiff: [], + unresolvedDiscussionsIdsByDate: [], + }; + + expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(true)).toBeFalsy(); + expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(false)).toBeFalsy(); + }); + }); + + describe('getDiscussion', () => { + it('returns discussion by ID', () => { + state.discussions.push({ id: '1' }); + + expect(getters.getDiscussion(state)('1')).toEqual({ id: '1' }); + }); + }); +}); diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js new file mode 100644 index 00000000000..49debe348e2 --- /dev/null +++ b/spec/frontend/notes/stores/mutation_spec.js @@ -0,0 +1,584 @@ +import Vue from 'vue'; +import mutations from '~/notes/stores/mutations'; +import { DISCUSSION_NOTE } from '~/notes/constants'; +import { + note, + discussionMock, + notesDataMock, + userDataMock, + noteableDataMock, + individualNote, +} from '../mock_data'; + +const RESOLVED_NOTE = { resolvable: true, resolved: true }; +const UNRESOLVED_NOTE = { resolvable: true, resolved: false }; +const SYSTEM_NOTE = { resolvable: false, resolved: false }; +const WEIRD_NOTE = { resolvable: false, resolved: true }; + +describe('Notes Store mutations', () => { + describe('ADD_NEW_NOTE', () => { + let state; + let noteData; + + beforeEach(() => { + state = { discussions: [] }; + noteData = { + expanded: true, + id: note.discussion_id, + individual_note: true, + notes: [note], + reply_id: note.discussion_id, + }; + mutations.ADD_NEW_NOTE(state, note); + }); + + it('should add a new note to an array of notes', () => { + expect(state).toEqual({ + discussions: [noteData], + }); + + expect(state.discussions.length).toBe(1); + }); + + it('should not add the same note to the notes array', () => { + mutations.ADD_NEW_NOTE(state, note); + + expect(state.discussions.length).toBe(1); + }); + }); + + describe('ADD_NEW_REPLY_TO_DISCUSSION', () => { + const newReply = Object.assign({}, note, { discussion_id: discussionMock.id }); + + let state; + + beforeEach(() => { + state = { discussions: [{ ...discussionMock }] }; + }); + + it('should add a reply to a specific discussion', () => { + mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); + + expect(state.discussions[0].notes.length).toEqual(4); + }); + + it('should not add the note if it already exists in the discussion', () => { + mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); + mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); + + expect(state.discussions[0].notes.length).toEqual(4); + }); + }); + + describe('DELETE_NOTE', () => { + it('should delete a note ', () => { + const state = { discussions: [discussionMock] }; + const toDelete = discussionMock.notes[0]; + const lengthBefore = discussionMock.notes.length; + + mutations.DELETE_NOTE(state, toDelete); + + expect(state.discussions[0].notes.length).toEqual(lengthBefore - 1); + }); + }); + + describe('EXPAND_DISCUSSION', () => { + it('should expand a collapsed discussion', () => { + const discussion = Object.assign({}, discussionMock, { expanded: false }); + + const state = { + discussions: [discussion], + }; + + mutations.EXPAND_DISCUSSION(state, { discussionId: discussion.id }); + + expect(state.discussions[0].expanded).toEqual(true); + }); + }); + + describe('COLLAPSE_DISCUSSION', () => { + it('should collapse an expanded discussion', () => { + const discussion = Object.assign({}, discussionMock, { expanded: true }); + + const state = { + discussions: [discussion], + }; + + mutations.COLLAPSE_DISCUSSION(state, { discussionId: discussion.id }); + + expect(state.discussions[0].expanded).toEqual(false); + }); + }); + + describe('REMOVE_PLACEHOLDER_NOTES', () => { + it('should remove all placeholder notes in indivudal notes and discussion', () => { + const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true }); + const state = { discussions: [placeholderNote] }; + mutations.REMOVE_PLACEHOLDER_NOTES(state); + + expect(state.discussions).toEqual([]); + }); + }); + + describe('SET_NOTES_DATA', () => { + it('should set an object with notesData', () => { + const state = { + notesData: {}, + }; + + mutations.SET_NOTES_DATA(state, notesDataMock); + + expect(state.notesData).toEqual(notesDataMock); + }); + }); + + describe('SET_NOTEABLE_DATA', () => { + it('should set the issue data', () => { + const state = { + noteableData: {}, + }; + + mutations.SET_NOTEABLE_DATA(state, noteableDataMock); + + expect(state.noteableData).toEqual(noteableDataMock); + }); + }); + + describe('SET_USER_DATA', () => { + it('should set the user data', () => { + const state = { + userData: {}, + }; + + mutations.SET_USER_DATA(state, userDataMock); + + expect(state.userData).toEqual(userDataMock); + }); + }); + + describe('SET_INITIAL_DISCUSSIONS', () => { + it('should set the initial notes received', () => { + const state = { + discussions: [], + }; + const legacyNote = { + id: 2, + individual_note: true, + notes: [ + { + note: '1', + }, + { + note: '2', + }, + ], + }; + + mutations.SET_INITIAL_DISCUSSIONS(state, [note, legacyNote]); + + expect(state.discussions[0].id).toEqual(note.id); + expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note); + expect(state.discussions[2].notes[0].note).toBe(legacyNote.notes[1].note); + expect(state.discussions.length).toEqual(3); + }); + + it('adds truncated_diff_lines if discussion is a diffFile', () => { + const state = { + discussions: [], + }; + + mutations.SET_INITIAL_DISCUSSIONS(state, [ + { + ...note, + diff_file: { + file_hash: 'a', + }, + truncated_diff_lines: [{ text: '+a', rich_text: '+a' }], + }, + ]); + + expect(state.discussions[0].truncated_diff_lines).toEqual([{ rich_text: 'a' }]); + }); + + it('adds empty truncated_diff_lines when not in discussion', () => { + const state = { + discussions: [], + }; + + mutations.SET_INITIAL_DISCUSSIONS(state, [ + { + ...note, + diff_file: { + file_hash: 'a', + }, + }, + ]); + + expect(state.discussions[0].truncated_diff_lines).toEqual([]); + }); + }); + + describe('SET_LAST_FETCHED_AT', () => { + it('should set timestamp', () => { + const state = { + lastFetchedAt: [], + }; + + mutations.SET_LAST_FETCHED_AT(state, 'timestamp'); + + expect(state.lastFetchedAt).toEqual('timestamp'); + }); + }); + + describe('SET_TARGET_NOTE_HASH', () => { + it('should set the note hash', () => { + const state = { + targetNoteHash: [], + }; + + mutations.SET_TARGET_NOTE_HASH(state, 'hash'); + + expect(state.targetNoteHash).toEqual('hash'); + }); + }); + + describe('SHOW_PLACEHOLDER_NOTE', () => { + it('should set a placeholder note', () => { + const state = { + discussions: [], + }; + mutations.SHOW_PLACEHOLDER_NOTE(state, note); + + expect(state.discussions[0].isPlaceholderNote).toEqual(true); + }); + }); + + describe('TOGGLE_AWARD', () => { + it('should add award if user has not reacted yet', () => { + const state = { + discussions: [note], + userData: userDataMock, + }; + + const data = { + note, + awardName: 'cartwheel', + }; + + mutations.TOGGLE_AWARD(state, data); + const lastIndex = state.discussions[0].award_emoji.length - 1; + + expect(state.discussions[0].award_emoji[lastIndex]).toEqual({ + name: 'cartwheel', + user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username }, + }); + }); + + it('should remove award if user already reacted', () => { + const state = { + discussions: [note], + userData: { + id: 1, + name: 'Administrator', + username: 'root', + }, + }; + + const data = { + note, + awardName: 'bath_tone3', + }; + mutations.TOGGLE_AWARD(state, data); + + expect(state.discussions[0].award_emoji.length).toEqual(2); + }); + }); + + describe('TOGGLE_DISCUSSION', () => { + it('should open a closed discussion', () => { + const discussion = Object.assign({}, discussionMock, { expanded: false }); + + const state = { + discussions: [discussion], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id }); + + expect(state.discussions[0].expanded).toEqual(true); + }); + + it('should close a opened discussion', () => { + const state = { + discussions: [discussionMock], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id }); + + expect(state.discussions[0].expanded).toEqual(false); + }); + + it('forces a discussions expanded state', () => { + const state = { + discussions: [{ ...discussionMock, expanded: false }], + }; + + mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id, forceExpanded: true }); + + expect(state.discussions[0].expanded).toEqual(true); + }); + }); + + describe('UPDATE_NOTE', () => { + it('should update a note', () => { + const state = { + discussions: [individualNote], + }; + + const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' }); + + mutations.UPDATE_NOTE(state, updated); + + expect(state.discussions[0].notes[0].note).toEqual('Foo'); + }); + + it('transforms an individual note to discussion', () => { + const state = { + discussions: [individualNote], + }; + + const transformedNote = { ...individualNote.notes[0], type: DISCUSSION_NOTE }; + + mutations.UPDATE_NOTE(state, transformedNote); + + expect(state.discussions[0].individual_note).toEqual(false); + }); + }); + + describe('CLOSE_ISSUE', () => { + it('should set issue as closed', () => { + const state = { + discussions: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: false, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.CLOSE_ISSUE(state); + + expect(state.noteableData.state).toEqual('closed'); + }); + }); + + describe('REOPEN_ISSUE', () => { + it('should set issue as closed', () => { + const state = { + discussions: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: false, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.REOPEN_ISSUE(state); + + expect(state.noteableData.state).toEqual('reopened'); + }); + }); + + describe('TOGGLE_STATE_BUTTON_LOADING', () => { + it('should set isToggleStateButtonLoading as true', () => { + const state = { + discussions: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: false, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.TOGGLE_STATE_BUTTON_LOADING(state, true); + + expect(state.isToggleStateButtonLoading).toEqual(true); + }); + + it('should set isToggleStateButtonLoading as false', () => { + const state = { + discussions: [], + targetNoteHash: null, + lastFetchedAt: null, + isToggleStateButtonLoading: true, + notesData: {}, + userData: {}, + noteableData: {}, + }; + + mutations.TOGGLE_STATE_BUTTON_LOADING(state, false); + + expect(state.isToggleStateButtonLoading).toEqual(false); + }); + }); + + describe('SET_NOTES_FETCHED_STATE', () => { + it('should set the given state', () => { + const state = { + isNotesFetched: false, + }; + + mutations.SET_NOTES_FETCHED_STATE(state, true); + + expect(state.isNotesFetched).toEqual(true); + }); + }); + + describe('SET_DISCUSSION_DIFF_LINES', () => { + it('sets truncated_diff_lines', () => { + const state = { + discussions: [ + { + id: 1, + }, + ], + }; + + mutations.SET_DISCUSSION_DIFF_LINES(state, { + discussionId: 1, + diffLines: [{ text: '+a', rich_text: '+a' }], + }); + + expect(state.discussions[0].truncated_diff_lines).toEqual([{ rich_text: 'a' }]); + }); + + it('keeps reactivity of discussion', () => { + const state = {}; + Vue.set(state, 'discussions', [ + { + id: 1, + expanded: false, + }, + ]); + const discussion = state.discussions[0]; + + mutations.SET_DISCUSSION_DIFF_LINES(state, { + discussionId: 1, + diffLines: [{ rich_text: 'a' }], + }); + + discussion.expanded = true; + + expect(state.discussions[0].expanded).toBe(true); + }); + }); + + describe('DISABLE_COMMENTS', () => { + it('should set comments disabled state', () => { + const state = {}; + + mutations.DISABLE_COMMENTS(state, true); + + expect(state.commentsDisabled).toEqual(true); + }); + }); + + describe('UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', () => { + it('with unresolvable discussions, updates state', () => { + const state = { + discussions: [ + { individual_note: false, resolvable: true, notes: [UNRESOLVED_NOTE] }, + { individual_note: true, resolvable: true, notes: [UNRESOLVED_NOTE] }, + { individual_note: false, resolvable: false, notes: [UNRESOLVED_NOTE] }, + ], + }; + + mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state); + + expect(state).toEqual( + expect.objectContaining({ + resolvableDiscussionsCount: 1, + unresolvedDiscussionsCount: 1, + hasUnresolvedDiscussions: false, + }), + ); + }); + + it('with resolvable discussions, updates state', () => { + const state = { + discussions: [ + { + individual_note: false, + resolvable: true, + notes: [RESOLVED_NOTE, SYSTEM_NOTE, RESOLVED_NOTE], + }, + { + individual_note: false, + resolvable: true, + notes: [RESOLVED_NOTE, SYSTEM_NOTE, WEIRD_NOTE], + }, + { + individual_note: false, + resolvable: true, + notes: [SYSTEM_NOTE, RESOLVED_NOTE, WEIRD_NOTE, UNRESOLVED_NOTE], + }, + { + individual_note: false, + resolvable: true, + notes: [UNRESOLVED_NOTE], + }, + ], + }; + + mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state); + + expect(state).toEqual( + expect.objectContaining({ + resolvableDiscussionsCount: 4, + unresolvedDiscussionsCount: 2, + hasUnresolvedDiscussions: true, + }), + ); + }); + }); + + describe('CONVERT_TO_DISCUSSION', () => { + let discussion; + let state; + + beforeEach(() => { + discussion = { + id: 42, + individual_note: true, + }; + state = { convertedDisscussionIds: [] }; + }); + + it('adds a discussion to convertedDisscussionIds', () => { + mutations.CONVERT_TO_DISCUSSION(state, discussion.id); + + expect(state.convertedDisscussionIds).toContain(discussion.id); + }); + }); + + describe('REMOVE_CONVERTED_DISCUSSION', () => { + let discussion; + let state; + + beforeEach(() => { + discussion = { + id: 42, + individual_note: true, + }; + state = { convertedDisscussionIds: [41, 42] }; + }); + + it('removes a discussion from convertedDisscussionIds', () => { + mutations.REMOVE_CONVERTED_DISCUSSION(state, discussion.id); + + expect(state.convertedDisscussionIds).not.toContain(discussion.id); + }); + }); +}); diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js new file mode 100644 index 00000000000..8917251d285 --- /dev/null +++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js @@ -0,0 +1,244 @@ +import $ from 'jquery'; +import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars +import TimezoneDropdown, { + formatUtcOffset, + formatTimezone, + findTimezoneByIdentifier, +} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown'; + +describe('Timezone Dropdown', () => { + preloadFixtures('pipeline_schedules/edit.html'); + + let $inputEl = null; + let $dropdownEl = null; + let $wrapper = null; + const tzListSel = '.dropdown-content ul li a.is-active'; + const tzDropdownToggleText = '.dropdown-toggle-text'; + + describe('Initialize', () => { + describe('with dropdown already loaded', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html'); + $wrapper = $('.dropdown'); + $inputEl = $('#schedule_cron_timezone'); + $dropdownEl = $('.js-timezone-dropdown'); + + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + }); + }); + + it('can take an $inputEl in the constructor', () => { + const tzStr = '[UTC + 5.5] Sri Jayawardenepura'; + const tzValue = 'Asia/Colombo'; + + expect($inputEl.val()).toBe('UTC'); + + $(`${tzListSel}:contains('${tzStr}')`, $wrapper).trigger('click'); + + const val = $inputEl.val(); + + expect(val).toBe(tzValue); + expect(val).not.toBe('UTC'); + }); + + it('will format data array of timezones into a list of offsets', () => { + const data = $dropdownEl.data('data'); + const formatted = $wrapper.find(tzListSel).text(); + + data.forEach(item => { + expect(formatted).toContain(formatTimezone(item)); + }); + }); + + it('will default the timezone to UTC', () => { + const tz = $inputEl.val(); + + expect(tz).toBe('UTC'); + }); + }); + + describe('without dropdown loaded', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html'); + $wrapper = $('.dropdown'); + $inputEl = $('#schedule_cron_timezone'); + $dropdownEl = $('.js-timezone-dropdown'); + }); + + it('will populate the list of UTC offsets after the dropdown is loaded', () => { + expect($wrapper.find(tzListSel).length).toEqual(0); + + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + }); + + expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length); + }); + + it('will call a provided handler when a new timezone is selected', () => { + const onSelectTimezone = jest.fn(); + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + onSelectTimezone, + }); + + $wrapper + .find(tzListSel) + .first() + .trigger('click'); + + expect(onSelectTimezone).toHaveBeenCalled(); + }); + + it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => { + $inputEl.val('America/St_Johns'); + + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + displayFormat: selectedItem => formatTimezone(selectedItem), + }); + + expect($wrapper.find(tzDropdownToggleText).html()).toEqual('[UTC - 2.5] Newfoundland'); + }); + + it('will call a provided `displayFormat` handler to format the dropdown value', () => { + const displayFormat = jest.fn(); + // eslint-disable-next-line no-new + new TimezoneDropdown({ + $inputEl, + $dropdownEl, + displayFormat, + }); + + $wrapper + .find(tzListSel) + .first() + .trigger('click'); + + expect(displayFormat).toHaveBeenCalled(); + }); + }); + }); + + describe('formatUtcOffset', () => { + it('will convert negative utc offsets in seconds to hours and minutes', () => { + expect(formatUtcOffset(-21600)).toEqual('- 6'); + }); + + it('will convert positive utc offsets in seconds to hours and minutes', () => { + expect(formatUtcOffset(25200)).toEqual('+ 7'); + expect(formatUtcOffset(49500)).toEqual('+ 13.75'); + }); + + it('will return 0 when given a string', () => { + expect(formatUtcOffset('BLAH')).toEqual('0'); + expect(formatUtcOffset('$%$%')).toEqual('0'); + }); + + it('will return 0 when given an array', () => { + expect(formatUtcOffset(['an', 'array'])).toEqual('0'); + }); + + it('will return 0 when given an object', () => { + expect(formatUtcOffset({ some: '', object: '' })).toEqual('0'); + }); + + it('will return 0 when given null', () => { + expect(formatUtcOffset(null)).toEqual('0'); + }); + + it('will return 0 when given undefined', () => { + expect(formatUtcOffset(undefined)).toEqual('0'); + }); + + it('will return 0 when given empty input', () => { + expect(formatUtcOffset('')).toEqual('0'); + }); + }); + + describe('formatTimezone', () => { + it('given name: "Chatham Is.", offset: "49500", will format for display as "[UTC + 13.75] Chatham Is."', () => { + expect( + formatTimezone({ + name: 'Chatham Is.', + offset: 49500, + identifier: 'Pacific/Chatham', + }), + ).toEqual('[UTC + 13.75] Chatham Is.'); + }); + + it('given name: "Saskatchewan", offset: "-21600", will format for display as "[UTC - 6] Saskatchewan"', () => { + expect( + formatTimezone({ + name: 'Saskatchewan', + offset: -21600, + identifier: 'America/Regina', + }), + ).toEqual('[UTC - 6] Saskatchewan'); + }); + + it('given name: "Accra", offset: "0", will format for display as "[UTC 0] Accra"', () => { + expect( + formatTimezone({ + name: 'Accra', + offset: 0, + identifier: 'Africa/Accra', + }), + ).toEqual('[UTC 0] Accra'); + }); + }); + + describe('findTimezoneByIdentifier', () => { + const tzList = [ + { + identifier: 'Asia/Tokyo', + name: 'Sapporo', + offset: 32400, + }, + { + identifier: 'Asia/Hong_Kong', + name: 'Hong Kong', + offset: 28800, + }, + { + identifier: 'Asia/Dhaka', + name: 'Dhaka', + offset: 21600, + }, + ]; + + const identifier = 'Asia/Dhaka'; + it('returns the correct object if the identifier exists', () => { + const res = findTimezoneByIdentifier(tzList, identifier); + + expect(res).toBeTruthy(); + expect(res).toBe(tzList[2]); + }); + + it('returns null if it doesnt find the identifier', () => { + const res = findTimezoneByIdentifier(tzList, 'Australia/Melbourne'); + + expect(res).toBeNull(); + }); + + it('returns null if there is no identifier given', () => { + expect(findTimezoneByIdentifier(tzList)).toBeNull(); + expect(findTimezoneByIdentifier(tzList, '')).toBeNull(); + }); + + it('returns null if there is an empty or invalid array given', () => { + expect(findTimezoneByIdentifier([], identifier)).toBeNull(); + expect(findTimezoneByIdentifier(null, identifier)).toBeNull(); + expect(findTimezoneByIdentifier(undefined, identifier)).toBeNull(); + }); + }); +}); diff --git a/spec/frontend/pipelines/nav_controls_spec.js b/spec/frontend/pipelines/nav_controls_spec.js new file mode 100644 index 00000000000..6d28da0ea2a --- /dev/null +++ b/spec/frontend/pipelines/nav_controls_spec.js @@ -0,0 +1,85 @@ +import Vue from 'vue'; +import navControlsComp from '~/pipelines/components/nav_controls.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Pipelines Nav Controls', () => { + let NavControlsComponent; + let component; + + beforeEach(() => { + NavControlsComponent = Vue.extend(navControlsComp); + }); + + afterEach(() => { + component.$destroy(); + }); + + it('should render link to create a new pipeline', () => { + const mockData = { + newPipelinePath: 'foo', + ciLintPath: 'foo', + resetCachePath: 'foo', + }; + + component = mountComponent(NavControlsComponent, mockData); + + expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline'); + expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual( + mockData.newPipelinePath, + ); + }); + + it('should not render link to create pipeline if no path is provided', () => { + const mockData = { + helpPagePath: 'foo', + ciLintPath: 'foo', + resetCachePath: 'foo', + }; + + component = mountComponent(NavControlsComponent, mockData); + + expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null); + }); + + it('should render link for CI lint', () => { + const mockData = { + newPipelinePath: 'foo', + helpPagePath: 'foo', + ciLintPath: 'foo', + resetCachePath: 'foo', + }; + + component = mountComponent(NavControlsComponent, mockData); + + expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint'); + expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual( + mockData.ciLintPath, + ); + }); + + describe('Reset Runners Cache', () => { + beforeEach(() => { + const mockData = { + newPipelinePath: 'foo', + ciLintPath: 'foo', + resetCachePath: 'foo', + }; + + component = mountComponent(NavControlsComponent, mockData); + }); + + it('should render button for resetting runner caches', () => { + expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain( + 'Clear Runner Caches', + ); + }); + + it('should emit postAction event when reset runner cache button is clicked', () => { + jest.spyOn(component, '$emit').mockImplementation(() => {}); + + component.$el.querySelector('.js-clear-cache').click(); + + expect(component.$emit).toHaveBeenCalledWith('resetRunnersCache', 'foo'); + }); + }); +}); diff --git a/spec/frontend/polyfills/element_spec.js b/spec/frontend/polyfills/element_spec.js new file mode 100644 index 00000000000..64ce248ca44 --- /dev/null +++ b/spec/frontend/polyfills/element_spec.js @@ -0,0 +1,46 @@ +import '~/commons/polyfills/element'; + +describe('Element polyfills', () => { + let testContext; + + beforeEach(() => { + testContext = {}; + }); + + beforeEach(() => { + testContext.element = document.createElement('ul'); + }); + + describe('matches', () => { + it('returns true if element matches the selector', () => { + expect(testContext.element.matches('ul')).toBeTruthy(); + }); + + it("returns false if element doesn't match the selector", () => { + expect(testContext.element.matches('.not-an-element')).toBeFalsy(); + }); + }); + + describe('closest', () => { + beforeEach(() => { + testContext.childElement = document.createElement('li'); + testContext.element.appendChild(testContext.childElement); + }); + + it('returns the closest parent that matches the selector', () => { + expect(testContext.childElement.closest('ul').toString()).toBe( + testContext.element.toString(), + ); + }); + + it('returns itself if it matches the selector', () => { + expect(testContext.childElement.closest('li').toString()).toBe( + testContext.childElement.toString(), + ); + }); + + it('returns undefined if nothing matches the selector', () => { + expect(testContext.childElement.closest('.no-an-element')).toBeFalsy(); + }); + }); +}); diff --git a/spec/frontend/profile/add_ssh_key_validation_spec.js b/spec/frontend/profile/add_ssh_key_validation_spec.js new file mode 100644 index 00000000000..1fec864599c --- /dev/null +++ b/spec/frontend/profile/add_ssh_key_validation_spec.js @@ -0,0 +1,71 @@ +import AddSshKeyValidation from '../../../app/assets/javascripts/profile/add_ssh_key_validation'; + +describe('AddSshKeyValidation', () => { + describe('submit', () => { + it('returns true if isValid is true', () => { + const addSshKeyValidation = new AddSshKeyValidation({}); + jest.spyOn(AddSshKeyValidation, 'isPublicKey').mockReturnValue(true); + + expect(addSshKeyValidation.submit()).toBeTruthy(); + }); + + it('calls preventDefault and toggleWarning if isValid is false', () => { + const addSshKeyValidation = new AddSshKeyValidation({}); + const event = { + preventDefault: jest.fn(), + }; + jest.spyOn(AddSshKeyValidation, 'isPublicKey').mockReturnValue(false); + jest.spyOn(addSshKeyValidation, 'toggleWarning').mockImplementation(() => {}); + + addSshKeyValidation.submit(event); + + expect(event.preventDefault).toHaveBeenCalled(); + expect(addSshKeyValidation.toggleWarning).toHaveBeenCalledWith(true); + }); + }); + + describe('toggleWarning', () => { + it('shows warningElement and hides originalSubmitElement if isVisible is true', () => { + const warningElement = document.createElement('div'); + const originalSubmitElement = document.createElement('div'); + warningElement.classList.add('hide'); + + const addSshKeyValidation = new AddSshKeyValidation( + {}, + warningElement, + originalSubmitElement, + ); + addSshKeyValidation.toggleWarning(true); + + expect(warningElement.classList.contains('hide')).toBeFalsy(); + expect(originalSubmitElement.classList.contains('hide')).toBeTruthy(); + }); + + it('hides warningElement and shows originalSubmitElement if isVisible is false', () => { + const warningElement = document.createElement('div'); + const originalSubmitElement = document.createElement('div'); + originalSubmitElement.classList.add('hide'); + + const addSshKeyValidation = new AddSshKeyValidation( + {}, + warningElement, + originalSubmitElement, + ); + addSshKeyValidation.toggleWarning(false); + + expect(warningElement.classList.contains('hide')).toBeTruthy(); + expect(originalSubmitElement.classList.contains('hide')).toBeFalsy(); + }); + }); + + describe('isPublicKey', () => { + it('returns false if probably invalid public ssh key', () => { + expect(AddSshKeyValidation.isPublicKey('nope')).toBeFalsy(); + }); + + it('returns true if probably valid public ssh key', () => { + expect(AddSshKeyValidation.isPublicKey('ssh-')).toBeTruthy(); + expect(AddSshKeyValidation.isPublicKey('ecdsa-sha2-')).toBeTruthy(); + }); + }); +}); diff --git a/spec/frontend/project_select_combo_button_spec.js b/spec/frontend/project_select_combo_button_spec.js new file mode 100644 index 00000000000..c47db71b4ac --- /dev/null +++ b/spec/frontend/project_select_combo_button_spec.js @@ -0,0 +1,140 @@ +import $ from 'jquery'; +import ProjectSelectComboButton from '~/project_select_combo_button'; + +const fixturePath = 'static/project_select_combo_button.html'; + +describe('Project Select Combo Button', () => { + let testContext; + + beforeEach(() => { + testContext = {}; + }); + + preloadFixtures(fixturePath); + + beforeEach(() => { + testContext.defaults = { + label: 'Select project to create issue', + groupId: 12345, + projectMeta: { + name: 'My Cool Project', + url: 'http://mycoolproject.com', + }, + newProjectMeta: { + name: 'My Other Cool Project', + url: 'http://myothercoolproject.com', + }, + localStorageKey: 'group-12345-new-issue-recent-project', + relativePath: 'issues/new', + }; + + loadFixtures(fixturePath); + + testContext.newItemBtn = document.querySelector('.new-project-item-link'); + testContext.projectSelectInput = document.querySelector('.project-item-select'); + }); + + describe('on page load when localStorage is empty', () => { + beforeEach(() => { + testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput); + }); + + it('newItemBtn href is null', () => { + expect(testContext.newItemBtn.getAttribute('href')).toBe(''); + }); + + it('newItemBtn text is the plain default label', () => { + expect(testContext.newItemBtn.textContent).toBe(testContext.defaults.label); + }); + }); + + describe('on page load when localStorage is filled', () => { + beforeEach(() => { + window.localStorage.setItem( + testContext.defaults.localStorageKey, + JSON.stringify(testContext.defaults.projectMeta), + ); + testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput); + }); + + it('newItemBtn href is correctly set', () => { + expect(testContext.newItemBtn.getAttribute('href')).toBe( + testContext.defaults.projectMeta.url, + ); + }); + + it('newItemBtn text is the cached label', () => { + expect(testContext.newItemBtn.textContent).toBe( + `New issue in ${testContext.defaults.projectMeta.name}`, + ); + }); + + afterEach(() => { + window.localStorage.clear(); + }); + }); + + describe('after selecting a new project', () => { + beforeEach(() => { + testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput); + + // mock the effect of selecting an item from the projects dropdown (select2) + $('.project-item-select') + .val(JSON.stringify(testContext.defaults.newProjectMeta)) + .trigger('change'); + }); + + it('newItemBtn href is correctly set', () => { + expect(testContext.newItemBtn.getAttribute('href')).toBe( + 'http://myothercoolproject.com/issues/new', + ); + }); + + it('newItemBtn text is the selected project label', () => { + expect(testContext.newItemBtn.textContent).toBe( + `New issue in ${testContext.defaults.newProjectMeta.name}`, + ); + }); + + afterEach(() => { + window.localStorage.clear(); + }); + }); + + describe('deriveTextVariants', () => { + beforeEach(() => { + testContext.mockExecutionContext = { + resourceType: '', + resourceLabel: '', + }; + + testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput); + + testContext.method = testContext.comboButton.deriveTextVariants.bind( + testContext.mockExecutionContext, + ); + }); + + it('correctly derives test variants for merge requests', () => { + testContext.mockExecutionContext.resourceType = 'merge_requests'; + testContext.mockExecutionContext.resourceLabel = 'New merge request'; + + const returnedVariants = testContext.method(); + + expect(returnedVariants.localStorageItemType).toBe('new-merge-request'); + expect(returnedVariants.defaultTextPrefix).toBe('New merge request'); + expect(returnedVariants.presetTextSuffix).toBe('merge request'); + }); + + it('correctly derives text variants for issues', () => { + testContext.mockExecutionContext.resourceType = 'issues'; + testContext.mockExecutionContext.resourceLabel = 'New issue'; + + const returnedVariants = testContext.method(); + + expect(returnedVariants.localStorageItemType).toBe('new-issue'); + expect(returnedVariants.defaultTextPrefix).toBe('New issue'); + expect(returnedVariants.presetTextSuffix).toBe('issue'); + }); + }); +}); diff --git a/spec/frontend/shared/popover_spec.js b/spec/frontend/shared/popover_spec.js new file mode 100644 index 00000000000..bbde936185e --- /dev/null +++ b/spec/frontend/shared/popover_spec.js @@ -0,0 +1,166 @@ +import $ from 'jquery'; +import { togglePopover, mouseleave, mouseenter } from '~/shared/popover'; + +describe('popover', () => { + describe('togglePopover', () => { + describe('togglePopover(true)', () => { + it('returns true when popover is shown', () => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + expect(togglePopover.call(context, true)).toEqual(true); + }); + + it('returns false when popover is already shown', () => { + const context = { + hasClass: () => true, + }; + + expect(togglePopover.call(context, true)).toEqual(false); + }); + + it('shows popover', done => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + jest.spyOn(context, 'popover').mockImplementation(method => { + expect(method).toEqual('show'); + done(); + }); + + togglePopover.call(context, true); + }); + + it('adds disable-animation and js-popover-show class', done => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + jest.spyOn(context, 'toggleClass').mockImplementation((classNames, show) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + expect(show).toEqual(true); + done(); + }); + + togglePopover.call(context, true); + }); + }); + + describe('togglePopover(false)', () => { + it('returns true when popover is hidden', () => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + expect(togglePopover.call(context, false)).toEqual(true); + }); + + it('returns false when popover is already hidden', () => { + const context = { + hasClass: () => false, + }; + + expect(togglePopover.call(context, false)).toEqual(false); + }); + + it('hides popover', done => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + jest.spyOn(context, 'popover').mockImplementation(method => { + expect(method).toEqual('hide'); + done(); + }); + + togglePopover.call(context, false); + }); + + it('removes disable-animation and js-popover-show class', done => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + jest.spyOn(context, 'toggleClass').mockImplementation((classNames, show) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + expect(show).toEqual(false); + done(); + }); + + togglePopover.call(context, false); + }); + }); + }); + + describe('mouseleave', () => { + it('calls hide popover if .popover:hover is false', () => { + const fakeJquery = { + length: 0, + }; + + jest + .spyOn($.fn, 'init') + .mockImplementation(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + jest.spyOn(togglePopover, 'call').mockImplementation(() => {}); + mouseleave(); + + expect(togglePopover.call).toHaveBeenCalledWith(expect.any(Object), false); + }); + + it('does not call hide popover if .popover:hover is true', () => { + const fakeJquery = { + length: 1, + }; + + jest + .spyOn($.fn, 'init') + .mockImplementation(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + jest.spyOn(togglePopover, 'call').mockImplementation(() => {}); + mouseleave(); + + expect(togglePopover.call).not.toHaveBeenCalledWith(false); + }); + }); + + describe('mouseenter', () => { + const context = {}; + + it('shows popover', () => { + jest.spyOn(togglePopover, 'call').mockReturnValue(false); + mouseenter.call(context); + + expect(togglePopover.call).toHaveBeenCalledWith(expect.any(Object), true); + }); + + it('registers mouseleave event if popover is showed', done => { + jest.spyOn(togglePopover, 'call').mockReturnValue(true); + jest.spyOn($.fn, 'on').mockImplementation(eventName => { + expect(eventName).toEqual('mouseleave'); + done(); + }); + mouseenter.call(context); + }); + + it('does not register mouseleave event if popover is not showed', () => { + jest.spyOn(togglePopover, 'call').mockReturnValue(false); + const spy = jest.spyOn($.fn, 'on').mockImplementation(() => {}); + mouseenter.call(context); + + expect(spy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/sidebar/sidebar_store_spec.js b/spec/frontend/sidebar/sidebar_store_spec.js new file mode 100644 index 00000000000..6d063a7cfcf --- /dev/null +++ b/spec/frontend/sidebar/sidebar_store_spec.js @@ -0,0 +1,168 @@ +import SidebarStore from '~/sidebar/stores/sidebar_store'; +import Mock from './mock_data'; +import UsersMockHelper from '../helpers/user_mock_data_helper'; + +const ASSIGNEE = { + id: 2, + name: 'gitlab user 2', + username: 'gitlab2', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', +}; + +const ANOTHER_ASSINEE = { + id: 3, + name: 'gitlab user 3', + username: 'gitlab3', + avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', +}; + +const PARTICIPANT = { + id: 1, + state: 'active', + username: 'marcene', + name: 'Allie Will', + web_url: 'foo.com', + avatar_url: 'gravatar.com/avatar/xxx', +}; + +const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }]; + +describe('Sidebar store', () => { + let testContext; + + beforeEach(() => { + testContext = {}; + }); + + beforeEach(() => { + testContext.store = new SidebarStore({ + currentUser: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + editable: true, + rootPath: '/', + endpoint: '/gitlab-org/gitlab-shell/issues/5.json', + }); + }); + + afterEach(() => { + SidebarStore.singleton = null; + }); + + it('has default isFetching values', () => { + expect(testContext.store.isFetching.assignees).toBe(true); + }); + + it('adds a new assignee', () => { + testContext.store.addAssignee(ASSIGNEE); + + expect(testContext.store.assignees.length).toEqual(1); + }); + + it('removes an assignee', () => { + testContext.store.removeAssignee(ASSIGNEE); + + expect(testContext.store.assignees.length).toEqual(0); + }); + + it('finds an existent assignee', () => { + let foundAssignee; + + testContext.store.addAssignee(ASSIGNEE); + foundAssignee = testContext.store.findAssignee(ASSIGNEE); + + expect(foundAssignee).toBeDefined(); + expect(foundAssignee).toEqual(ASSIGNEE); + foundAssignee = testContext.store.findAssignee(ANOTHER_ASSINEE); + + expect(foundAssignee).toBeUndefined(); + }); + + it('removes all assignees', () => { + testContext.store.removeAllAssignees(); + + expect(testContext.store.assignees.length).toEqual(0); + }); + + it('sets participants data', () => { + expect(testContext.store.participants.length).toEqual(0); + + testContext.store.setParticipantsData({ + participants: PARTICIPANT_LIST, + }); + + expect(testContext.store.isFetching.participants).toEqual(false); + expect(testContext.store.participants.length).toEqual(PARTICIPANT_LIST.length); + }); + + it('sets subcriptions data', () => { + expect(testContext.store.subscribed).toEqual(null); + + testContext.store.setSubscriptionsData({ + subscribed: true, + }); + + expect(testContext.store.isFetching.subscriptions).toEqual(false); + expect(testContext.store.subscribed).toEqual(true); + }); + + it('set assigned data', () => { + const users = { + assignees: UsersMockHelper.createNumberRandomUsers(3), + }; + + testContext.store.setAssigneeData(users); + + expect(testContext.store.isFetching.assignees).toBe(false); + expect(testContext.store.assignees.length).toEqual(3); + }); + + it('sets fetching state', () => { + expect(testContext.store.isFetching.participants).toEqual(true); + + testContext.store.setFetchingState('participants', false); + + expect(testContext.store.isFetching.participants).toEqual(false); + }); + + it('sets loading state', () => { + testContext.store.setLoadingState('assignees', true); + + expect(testContext.store.isLoading.assignees).toEqual(true); + }); + + it('set time tracking data', () => { + testContext.store.setTimeTrackingData(Mock.time); + + expect(testContext.store.timeEstimate).toEqual(Mock.time.time_estimate); + expect(testContext.store.totalTimeSpent).toEqual(Mock.time.total_time_spent); + expect(testContext.store.humanTimeEstimate).toEqual(Mock.time.human_time_estimate); + expect(testContext.store.humanTotalTimeSpent).toEqual(Mock.time.human_total_time_spent); + }); + + it('set autocomplete projects', () => { + const projects = [{ id: 0 }]; + testContext.store.setAutocompleteProjects(projects); + + expect(testContext.store.autocompleteProjects).toEqual(projects); + }); + + it('sets subscribed state', () => { + expect(testContext.store.subscribed).toEqual(null); + + testContext.store.setSubscribedState(true); + + expect(testContext.store.subscribed).toEqual(true); + }); + + it('set move to project ID', () => { + const projectId = 7; + testContext.store.setMoveToProjectId(projectId); + + expect(testContext.store.moveToProjectId).toEqual(projectId); + }); +}); diff --git a/spec/frontend/syntax_highlight_spec.js b/spec/frontend/syntax_highlight_spec.js new file mode 100644 index 00000000000..d2fb5983f7b --- /dev/null +++ b/spec/frontend/syntax_highlight_spec.js @@ -0,0 +1,48 @@ +/* eslint-disable no-return-assign */ + +import $ from 'jquery'; +import syntaxHighlight from '~/syntax_highlight'; + +describe('Syntax Highlighter', () => { + const stubUserColorScheme = value => { + if (window.gon == null) { + window.gon = {}; + } + return (window.gon.user_color_scheme = value); + }; + describe('on a js-syntax-highlight element', () => { + beforeEach(() => { + setFixtures('
'); + }); + + it('applies syntax highlighting', () => { + stubUserColorScheme('monokai'); + syntaxHighlight($('.js-syntax-highlight')); + + expect($('.js-syntax-highlight')).toHaveClass('monokai'); + }); + }); + + describe('on a parent element', () => { + beforeEach(() => { + setFixtures( + '
\n
\n
\n
\n
', + ); + }); + + it('applies highlighting to all applicable children', () => { + stubUserColorScheme('monokai'); + syntaxHighlight($('.parent')); + + expect($('.parent, .foo')).not.toHaveClass('monokai'); + expect($('.monokai').length).toBe(2); + }); + + it('prevents an infinite loop when no matches exist', () => { + setFixtures('
'); + const highlight = () => syntaxHighlight($('div')); + + expect(highlight).not.toThrow(); + }); + }); +}); diff --git a/spec/frontend/task_list_spec.js b/spec/frontend/task_list_spec.js new file mode 100644 index 00000000000..1261833e3ec --- /dev/null +++ b/spec/frontend/task_list_spec.js @@ -0,0 +1,156 @@ +import $ from 'jquery'; +import TaskList from '~/task_list'; +import axios from '~/lib/utils/axios_utils'; + +describe('TaskList', () => { + let taskList; + let currentTarget; + const taskListOptions = { + selector: '.task-list', + dataType: 'issue', + fieldName: 'description', + lockVersion: 2, + }; + const createTaskList = () => new TaskList(taskListOptions); + + beforeEach(() => { + setFixtures(` +
+
+
+ `); + + currentTarget = $('
'); + taskList = createTaskList(); + }); + + it('should call init when the class constructed', () => { + jest.spyOn(TaskList.prototype, 'init'); + jest.spyOn(TaskList.prototype, 'disable').mockImplementation(() => {}); + jest.spyOn($.prototype, 'taskList').mockImplementation(() => {}); + jest.spyOn($.prototype, 'on').mockImplementation(() => {}); + + taskList = createTaskList(); + const $taskListEl = $(taskList.taskListContainerSelector); + + expect(taskList.init).toHaveBeenCalled(); + expect(taskList.disable).toHaveBeenCalled(); + expect($taskListEl.taskList).toHaveBeenCalledWith('enable'); + expect($(document).on).toHaveBeenCalledWith( + 'tasklist:changed', + taskList.taskListContainerSelector, + taskList.updateHandler, + ); + }); + + describe('getTaskListTarget', () => { + it('should return currentTarget from event object if exists', () => { + const $target = taskList.getTaskListTarget({ currentTarget }); + + expect($target).toEqual(currentTarget); + }); + + it('should return element of the taskListContainerSelector', () => { + const $target = taskList.getTaskListTarget(); + + expect($target).toEqual($(taskList.taskListContainerSelector)); + }); + }); + + describe('disableTaskListItems', () => { + it('should call taskList method with disable param', () => { + jest.spyOn($.prototype, 'taskList').mockImplementation(() => {}); + + taskList.disableTaskListItems({ currentTarget }); + + expect(currentTarget.taskList).toHaveBeenCalledWith('disable'); + }); + }); + + describe('enableTaskListItems', () => { + it('should call taskList method with enable param', () => { + jest.spyOn($.prototype, 'taskList').mockImplementation(() => {}); + + taskList.enableTaskListItems({ currentTarget }); + + expect(currentTarget.taskList).toHaveBeenCalledWith('enable'); + }); + }); + + describe('disable', () => { + it('should disable task list items and off document event', () => { + jest.spyOn(taskList, 'disableTaskListItems').mockImplementation(() => {}); + jest.spyOn($.prototype, 'off').mockImplementation(() => {}); + + taskList.disable(); + + expect(taskList.disableTaskListItems).toHaveBeenCalled(); + expect($(document).off).toHaveBeenCalledWith( + 'tasklist:changed', + taskList.taskListContainerSelector, + ); + }); + }); + + describe('update', () => { + it('should disable task list items and make a patch request then enable them again', done => { + const response = { data: { lock_version: 3 } }; + jest.spyOn(taskList, 'enableTaskListItems').mockImplementation(() => {}); + jest.spyOn(taskList, 'disableTaskListItems').mockImplementation(() => {}); + jest.spyOn(taskList, 'onSuccess').mockImplementation(() => {}); + jest.spyOn(axios, 'patch').mockReturnValue(Promise.resolve(response)); + + const value = 'hello world'; + const endpoint = '/foo'; + const target = $(``); + const detail = { + index: 2, + checked: true, + lineNumber: 8, + lineSource: '- [ ] check item', + }; + const event = { target, detail }; + const patchData = { + [taskListOptions.dataType]: { + [taskListOptions.fieldName]: value, + lock_version: taskListOptions.lockVersion, + update_task: { + index: detail.index, + checked: detail.checked, + line_number: detail.lineNumber, + line_source: detail.lineSource, + }, + }, + }; + + taskList + .update(event) + .then(() => { + expect(taskList.disableTaskListItems).toHaveBeenCalledWith(event); + expect(axios.patch).toHaveBeenCalledWith(endpoint, patchData); + expect(taskList.enableTaskListItems).toHaveBeenCalledWith(event); + expect(taskList.onSuccess).toHaveBeenCalledWith(response.data); + expect(taskList.lockVersion).toEqual(response.data.lock_version); + }) + .then(done) + .catch(done.fail); + }); + }); + + it('should handle request error and enable task list items', done => { + const response = { data: { error: 1 } }; + jest.spyOn(taskList, 'enableTaskListItems').mockImplementation(() => {}); + jest.spyOn(taskList, 'onError').mockImplementation(() => {}); + jest.spyOn(axios, 'patch').mockReturnValue(Promise.reject({ response })); // eslint-disable-line prefer-promise-reject-errors + + const event = { detail: {} }; + taskList + .update(event) + .then(() => { + expect(taskList.enableTaskListItems).toHaveBeenCalledWith(event); + expect(taskList.onError).toHaveBeenCalledWith(response.data); + }) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/frontend/version_check_image_spec.js b/spec/frontend/version_check_image_spec.js new file mode 100644 index 00000000000..2ab157105a1 --- /dev/null +++ b/spec/frontend/version_check_image_spec.js @@ -0,0 +1,42 @@ +import $ from 'jquery'; +import VersionCheckImage from '~/version_check_image'; +import ClassSpecHelper from './helpers/class_spec_helper'; + +describe('VersionCheckImage', () => { + let testContext; + + beforeEach(() => { + testContext = {}; + }); + + describe('bindErrorEvent', () => { + ClassSpecHelper.itShouldBeAStaticMethod(VersionCheckImage, 'bindErrorEvent'); + + beforeEach(() => { + testContext.imageElement = $('
'); + }); + + it('registers an error event', () => { + jest.spyOn($.prototype, 'on').mockImplementation(() => {}); + // eslint-disable-next-line func-names + jest.spyOn($.prototype, 'off').mockImplementation(function() { + return this; + }); + + VersionCheckImage.bindErrorEvent(testContext.imageElement); + + expect($.prototype.off).toHaveBeenCalledWith('error'); + expect($.prototype.on).toHaveBeenCalledWith('error', expect.any(Function)); + }); + + it('hides the imageElement on error', () => { + jest.spyOn($.prototype, 'hide').mockImplementation(() => {}); + + VersionCheckImage.bindErrorEvent(testContext.imageElement); + + testContext.imageElement.trigger('error'); + + expect($.prototype.hide).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js new file mode 100644 index 00000000000..4b7636041b6 --- /dev/null +++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js @@ -0,0 +1,151 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; +import { GlModal } from '@gitlab/ui'; +import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue'; +import createState from '~/vuex_shared/modules/modal/state'; + +const localVue = createLocalVue(); +localVue.use(Vuex); + +const TEST_SLOT = 'Lorem ipsum modal dolar sit.'; +const TEST_MODAL_ID = 'my-modal-id'; +const TEST_MODULE = 'myModal'; + +describe('GlModalVuex', () => { + let wrapper; + let state; + let actions; + + const factory = (options = {}) => { + const store = new Vuex.Store({ + modules: { + [TEST_MODULE]: { + namespaced: true, + state, + actions, + }, + }, + }); + + const propsData = { + modalId: TEST_MODAL_ID, + modalModule: TEST_MODULE, + ...options.propsData, + }; + + wrapper = shallowMount(localVue.extend(GlModalVuex), { + ...options, + localVue, + store, + propsData, + }); + }; + + beforeEach(() => { + state = createState(); + + actions = { + show: jest.fn(), + hide: jest.fn(), + }; + }); + + it('renders gl-modal', () => { + factory({ + slots: { + default: `
${TEST_SLOT}
`, + }, + }); + const glModal = wrapper.find(GlModal); + + expect(glModal.props('modalId')).toBe(TEST_MODAL_ID); + expect(glModal.text()).toContain(TEST_SLOT); + }); + + it('passes props through to gl-modal', () => { + const title = 'Test Title'; + const okVariant = 'success'; + + factory({ + propsData: { + title, + okTitle: title, + okVariant, + }, + }); + const glModal = wrapper.find(GlModal); + + expect(glModal.attributes('title')).toEqual(title); + expect(glModal.attributes('oktitle')).toEqual(title); + expect(glModal.attributes('okvariant')).toEqual(okVariant); + }); + + it('passes listeners through to gl-modal', () => { + const ok = jest.fn(); + + factory({ + listeners: { ok }, + }); + + const glModal = wrapper.find(GlModal); + glModal.vm.$emit('ok'); + + expect(ok).toHaveBeenCalledTimes(1); + }); + + it('calls vuex action on show', () => { + expect(actions.show).not.toHaveBeenCalled(); + + factory(); + + const glModal = wrapper.find(GlModal); + glModal.vm.$emit('shown'); + + expect(actions.show).toHaveBeenCalledTimes(1); + }); + + it('calls vuex action on hide', () => { + expect(actions.hide).not.toHaveBeenCalled(); + + factory(); + + const glModal = wrapper.find(GlModal); + glModal.vm.$emit('hidden'); + + expect(actions.hide).toHaveBeenCalledTimes(1); + }); + + it('calls bootstrap show when isVisible changes', done => { + state.isVisible = false; + + factory(); + const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit'); + + state.isVisible = true; + + localVue + .nextTick() + .then(() => { + expect(rootEmit).toHaveBeenCalledWith('bv::show::modal', TEST_MODAL_ID); + }) + .then(done) + .catch(done.fail); + }); + + it('calls bootstrap hide when isVisible changes', done => { + state.isVisible = true; + + factory(); + const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit'); + + state.isVisible = false; + + localVue + .nextTick() + .then(() => { + expect(rootEmit).toHaveBeenCalledWith('bv::hide::modal', TEST_MODAL_ID); + }) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js new file mode 100644 index 00000000000..3c5e7500ba7 --- /dev/null +++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_spec.js @@ -0,0 +1,101 @@ +import Vue from 'vue'; +import SuggestionDiffComponent from '~/vue_shared/components/markdown/suggestion_diff.vue'; +import { selectDiffLines } from '~/vue_shared/components/lib/utils/diff_utils'; + +const MOCK_DATA = { + canApply: true, + suggestion: { + id: 1, + diff_lines: [ + { + can_receive_suggestion: false, + line_code: null, + meta_data: null, + new_line: null, + old_line: 5, + rich_text: '-test', + text: '-test', + type: 'old', + }, + { + can_receive_suggestion: true, + line_code: null, + meta_data: null, + new_line: 5, + old_line: null, + rich_text: '+new test', + text: '+new test', + type: 'new', + }, + { + can_receive_suggestion: true, + line_code: null, + meta_data: null, + new_line: 5, + old_line: null, + rich_text: '+new test2', + text: '+new test2', + type: 'new', + }, + ], + }, + helpPagePath: 'path_to_docs', +}; + +const lines = selectDiffLines(MOCK_DATA.suggestion.diff_lines); +const newLines = lines.filter(line => line.type === 'new'); + +describe('Suggestion Diff component', () => { + let vm; + + beforeEach(done => { + const Component = Vue.extend(SuggestionDiffComponent); + + vm = new Component({ + propsData: MOCK_DATA, + }).$mount(); + + Vue.nextTick(done); + }); + + describe('init', () => { + it('renders a suggestion header', () => { + expect(vm.$el.querySelector('.js-suggestion-diff-header')).not.toBeNull(); + }); + + it('renders a diff table with syntax highlighting', () => { + expect(vm.$el.querySelector('.md-suggestion-diff.js-syntax-highlight.code')).not.toBeNull(); + }); + + it('renders the oldLineNumber', () => { + const fromLine = vm.$el.querySelector('.old_line').innerHTML; + + expect(parseInt(fromLine, 10)).toBe(lines[0].old_line); + }); + + it('renders the oldLineContent', () => { + const fromContent = vm.$el.querySelector('.line_content.old').innerHTML; + + expect(fromContent.includes(lines[0].text)).toBe(true); + }); + + it('renders new lines', () => { + const newLinesElements = vm.$el.querySelectorAll('.line_holder.new'); + + newLinesElements.forEach((line, i) => { + expect(newLinesElements[i].innerHTML.includes(newLines[i].new_line)).toBe(true); + expect(newLinesElements[i].innerHTML.includes(newLines[i].text)).toBe(true); + }); + }); + }); + + describe('applySuggestion', () => { + it('emits apply event when applySuggestion is called', () => { + const callback = () => {}; + jest.spyOn(vm, '$emit').mockImplementation(() => {}); + vm.applySuggestion(callback); + + expect(vm.$emit).toHaveBeenCalledWith('apply', { suggestionId: vm.suggestion.id, callback }); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js new file mode 100644 index 00000000000..9f0cdc651b6 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js @@ -0,0 +1,156 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { GlButton } from '@gitlab/ui'; +import { TEST_HOST } from 'spec/test_constants'; +import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; + +const TEST_IMAGE_SIZE = 7; +const TEST_BREAKPOINT = 5; +const TEST_EMPTY_MESSAGE = 'Lorem ipsum empty'; +const DEFAULT_EMPTY_MESSAGE = 'None'; + +const createUser = id => ({ + id, + name: 'Lorem', + web_url: `${TEST_HOST}/${id}`, + avatar_url: `${TEST_HOST}/${id}/avatar`, +}); +const createList = n => + Array(n) + .fill(1) + .map((x, id) => createUser(id)); + +const localVue = createLocalVue(); + +describe('UserAvatarList', () => { + let props; + let wrapper; + + const factory = (options = {}) => { + const propsData = { + ...props, + ...options.propsData, + }; + + wrapper = shallowMount(localVue.extend(UserAvatarList), { + ...options, + localVue, + propsData, + }); + }; + + const clickButton = () => { + const button = wrapper.find(GlButton); + button.vm.$emit('click'); + }; + + beforeEach(() => { + props = { imgSize: TEST_IMAGE_SIZE }; + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('empty text', () => { + it('shows when items are empty', () => { + factory({ propsData: { items: [] } }); + + expect(wrapper.text()).toContain(DEFAULT_EMPTY_MESSAGE); + }); + + it('does not show when items are not empty', () => { + factory({ propsData: { items: createList(1) } }); + + expect(wrapper.text()).not.toContain(DEFAULT_EMPTY_MESSAGE); + }); + + it('can be set in props', () => { + factory({ propsData: { items: [], emptyText: TEST_EMPTY_MESSAGE } }); + + expect(wrapper.text()).toContain(TEST_EMPTY_MESSAGE); + }); + }); + + describe('with no breakpoint', () => { + beforeEach(() => { + props.breakpoint = 0; + }); + + it('renders avatars', () => { + const items = createList(20); + factory({ propsData: { items } }); + + const links = wrapper.findAll(UserAvatarLink); + const linkProps = links.wrappers.map(x => x.props()); + + expect(linkProps).toEqual( + items.map(x => + expect.objectContaining({ + linkHref: x.web_url, + imgSrc: x.avatar_url, + imgAlt: x.name, + tooltipText: x.name, + imgSize: TEST_IMAGE_SIZE, + }), + ), + ); + }); + }); + + describe('with breakpoint and length equal to breakpoint', () => { + beforeEach(() => { + props.breakpoint = TEST_BREAKPOINT; + props.items = createList(TEST_BREAKPOINT); + }); + + it('renders all avatars if length is <= breakpoint', () => { + factory(); + + const links = wrapper.findAll(UserAvatarLink); + + expect(links.length).toEqual(props.items.length); + }); + + it('does not show button', () => { + factory(); + + expect(wrapper.find(GlButton).exists()).toBe(false); + }); + }); + + describe('with breakpoint and length greater than breakpoint', () => { + beforeEach(() => { + props.breakpoint = TEST_BREAKPOINT; + props.items = createList(TEST_BREAKPOINT + 1); + }); + + it('renders avatars up to breakpoint', () => { + factory(); + + const links = wrapper.findAll(UserAvatarLink); + + expect(links.length).toEqual(TEST_BREAKPOINT); + }); + + describe('with expand clicked', () => { + beforeEach(() => { + factory(); + clickButton(); + }); + + it('renders all avatars', () => { + const links = wrapper.findAll(UserAvatarLink); + + expect(links.length).toEqual(props.items.length); + }); + + it('with collapse clicked, it renders avatars up to breakpoint', () => { + clickButton(); + const links = wrapper.findAll(UserAvatarLink); + + expect(links.length).toEqual(TEST_BREAKPOINT); + }); + }); + }); +}); diff --git a/spec/javascripts/behaviors/bind_in_out_spec.js b/spec/javascripts/behaviors/bind_in_out_spec.js deleted file mode 100644 index 0c214f5886a..00000000000 --- a/spec/javascripts/behaviors/bind_in_out_spec.js +++ /dev/null @@ -1,192 +0,0 @@ -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/bootstrap_jquery_spec.js b/spec/javascripts/bootstrap_jquery_spec.js deleted file mode 100644 index 6957cf40301..00000000000 --- a/spec/javascripts/bootstrap_jquery_spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import $ from 'jquery'; -import '~/commons/bootstrap'; - -describe('Bootstrap jQuery extensions', function() { - describe('disable', function() { - beforeEach(function() { - return setFixtures(''); - }); - - it('adds the disabled attribute', function() { - const $input = $('input').first(); - $input.disable(); - - expect($input).toHaveAttr('disabled', 'disabled'); - }); - return it('adds the disabled class', function() { - const $input = $('input').first(); - $input.disable(); - - expect($input).toHaveClass('disabled'); - }); - }); - return describe('enable', function() { - beforeEach(function() { - return setFixtures(''); - }); - - it('removes the disabled attribute', function() { - const $input = $('input').first(); - $input.enable(); - - expect($input).not.toHaveAttr('disabled'); - }); - return it('removes the disabled class', function() { - const $input = $('input').first(); - $input.enable(); - - expect($input).not.toHaveClass('disabled'); - }); - }); -}); diff --git a/spec/javascripts/branches/branches_delete_modal_spec.js b/spec/javascripts/branches/branches_delete_modal_spec.js deleted file mode 100644 index b223b8e2c0a..00000000000 --- a/spec/javascripts/branches/branches_delete_modal_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import $ from 'jquery'; -import DeleteModal from '~/branches/branches_delete_modal'; - -describe('branches delete modal', () => { - describe('setDisableDeleteButton', () => { - let submitSpy; - let $deleteButton; - - beforeEach(() => { - setFixtures(` - - `); - $deleteButton = $('.js-delete-branch'); - submitSpy = jasmine.createSpy('submit').and.callFake(event => event.preventDefault()); - $('#modal-delete-branch form').on('submit', submitSpy); - // eslint-disable-next-line no-new - new DeleteModal(); - }); - - it('does not submit if button is disabled', () => { - $deleteButton.attr('disabled', true); - - $deleteButton.click(); - - expect(submitSpy).not.toHaveBeenCalled(); - }); - - it('submits if button is not disabled', () => { - $deleteButton.attr('disabled', false); - - $deleteButton.click(); - - expect(submitSpy).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/javascripts/breakpoints_spec.js b/spec/javascripts/breakpoints_spec.js deleted file mode 100644 index fc0d9eb907a..00000000000 --- a/spec/javascripts/breakpoints_spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import bp, { breakpoints } from '~/breakpoints'; - -describe('breakpoints', () => { - Object.keys(breakpoints).forEach(key => { - const size = breakpoints[key]; - - it(`returns ${key} when larger than ${size}`, () => { - spyOn(bp, 'windowWidth').and.returnValue(size + 10); - - expect(bp.getBreakpointSize()).toBe(key); - }); - }); - - describe('isDesktop', () => { - it('returns true when screen size is medium', () => { - spyOn(bp, 'windowWidth').and.returnValue(breakpoints.md + 10); - - expect(bp.isDesktop()).toBe(true); - }); - - it('returns false when screen size is small', () => { - spyOn(bp, 'windowWidth').and.returnValue(breakpoints.sm + 10); - - expect(bp.isDesktop()).toBe(false); - }); - }); -}); diff --git a/spec/javascripts/diffs/components/settings_dropdown_spec.js b/spec/javascripts/diffs/components/settings_dropdown_spec.js deleted file mode 100644 index 6c08474ffd2..00000000000 --- a/spec/javascripts/diffs/components/settings_dropdown_spec.js +++ /dev/null @@ -1,167 +0,0 @@ -import { mount, createLocalVue } from '@vue/test-utils'; -import Vuex from 'vuex'; -import diffModule from '~/diffs/store/modules'; -import SettingsDropdown from '~/diffs/components/settings_dropdown.vue'; -import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants'; - -const localVue = createLocalVue(); -localVue.use(Vuex); - -describe('Diff settiings dropdown component', () => { - let vm; - let actions; - - function createComponent(extendStore = () => {}) { - const store = new Vuex.Store({ - modules: { - diffs: { - namespaced: true, - actions, - state: diffModule().state, - getters: diffModule().getters, - }, - }, - }); - - extendStore(store); - - vm = mount(localVue.extend(SettingsDropdown), { - localVue, - store, - sync: false, - }); - } - - beforeEach(() => { - actions = { - setInlineDiffViewType: jasmine.createSpy('setInlineDiffViewType'), - setParallelDiffViewType: jasmine.createSpy('setParallelDiffViewType'), - setRenderTreeList: jasmine.createSpy('setRenderTreeList'), - setShowWhitespace: jasmine.createSpy('setShowWhitespace'), - }; - }); - - afterEach(() => { - vm.destroy(); - }); - - describe('tree view buttons', () => { - it('list view button dispatches setRenderTreeList with false', () => { - createComponent(); - - vm.find('.js-list-view').trigger('click'); - - expect(actions.setRenderTreeList).toHaveBeenCalledWith(jasmine.anything(), false, undefined); - }); - - it('tree view button dispatches setRenderTreeList with true', () => { - createComponent(); - - vm.find('.js-tree-view').trigger('click'); - - expect(actions.setRenderTreeList).toHaveBeenCalledWith(jasmine.anything(), true, undefined); - }); - - it('sets list button as active when renderTreeList is false', () => { - createComponent(store => { - Object.assign(store.state.diffs, { - renderTreeList: false, - }); - }); - - expect(vm.find('.js-list-view').classes('active')).toBe(true); - expect(vm.find('.js-tree-view').classes('active')).toBe(false); - }); - - it('sets tree button as active when renderTreeList is true', () => { - createComponent(store => { - Object.assign(store.state.diffs, { - renderTreeList: true, - }); - }); - - expect(vm.find('.js-list-view').classes('active')).toBe(false); - expect(vm.find('.js-tree-view').classes('active')).toBe(true); - }); - }); - - describe('compare changes', () => { - it('sets inline button as active', () => { - createComponent(store => { - Object.assign(store.state.diffs, { - diffViewType: INLINE_DIFF_VIEW_TYPE, - }); - }); - - expect(vm.find('.js-inline-diff-button').classes('active')).toBe(true); - expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(false); - }); - - it('sets parallel button as active', () => { - createComponent(store => { - Object.assign(store.state.diffs, { - diffViewType: PARALLEL_DIFF_VIEW_TYPE, - }); - }); - - expect(vm.find('.js-inline-diff-button').classes('active')).toBe(false); - expect(vm.find('.js-parallel-diff-button').classes('active')).toBe(true); - }); - - it('calls setInlineDiffViewType when clicking inline button', () => { - createComponent(); - - vm.find('.js-inline-diff-button').trigger('click'); - - expect(actions.setInlineDiffViewType).toHaveBeenCalled(); - }); - - it('calls setParallelDiffViewType when clicking parallel button', () => { - createComponent(); - - vm.find('.js-parallel-diff-button').trigger('click'); - - expect(actions.setParallelDiffViewType).toHaveBeenCalled(); - }); - }); - - describe('whitespace toggle', () => { - it('does not set as checked when showWhitespace is false', () => { - createComponent(store => { - Object.assign(store.state.diffs, { - showWhitespace: false, - }); - }); - - expect(vm.find('#show-whitespace').element.checked).toBe(false); - }); - - it('sets as checked when showWhitespace is true', () => { - createComponent(store => { - Object.assign(store.state.diffs, { - showWhitespace: true, - }); - }); - - expect(vm.find('#show-whitespace').element.checked).toBe(true); - }); - - it('calls setShowWhitespace on change', () => { - createComponent(); - - const checkbox = vm.find('#show-whitespace'); - - checkbox.element.checked = true; - checkbox.trigger('change'); - - expect(actions.setShowWhitespace).toHaveBeenCalledWith( - jasmine.anything(), - { - showWhitespace: true, - pushState: true, - }, - undefined, - ); - }); - }); -}); diff --git a/spec/javascripts/droplab/constants_spec.js b/spec/javascripts/droplab/constants_spec.js deleted file mode 100644 index 23b69defec6..00000000000 --- a/spec/javascripts/droplab/constants_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import * as constants from '~/droplab/constants'; - -describe('constants', function() { - describe('DATA_TRIGGER', function() { - it('should be `data-dropdown-trigger`', function() { - expect(constants.DATA_TRIGGER).toBe('data-dropdown-trigger'); - }); - }); - - describe('DATA_DROPDOWN', function() { - it('should be `data-dropdown`', function() { - expect(constants.DATA_DROPDOWN).toBe('data-dropdown'); - }); - }); - - describe('SELECTED_CLASS', function() { - it('should be `droplab-item-selected`', function() { - expect(constants.SELECTED_CLASS).toBe('droplab-item-selected'); - }); - }); - - describe('ACTIVE_CLASS', function() { - it('should be `droplab-item-active`', function() { - expect(constants.ACTIVE_CLASS).toBe('droplab-item-active'); - }); - }); - - describe('TEMPLATE_REGEX', function() { - it('should be a handlebars templating syntax regex', function() { - expect(constants.TEMPLATE_REGEX).toEqual(/\{\{(.+?)\}\}/g); - }); - }); - - describe('IGNORE_CLASS', function() { - it('should be `droplab-item-ignore`', function() { - expect(constants.IGNORE_CLASS).toBe('droplab-item-ignore'); - }); - }); -}); diff --git a/spec/javascripts/droplab/plugins/ajax_filter_spec.js b/spec/javascripts/droplab/plugins/ajax_filter_spec.js deleted file mode 100644 index 5dbe50af07f..00000000000 --- a/spec/javascripts/droplab/plugins/ajax_filter_spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import AjaxCache from '~/lib/utils/ajax_cache'; -import AjaxFilter from '~/droplab/plugins/ajax_filter'; - -describe('AjaxFilter', () => { - let dummyConfig; - const dummyData = 'dummy data'; - let dummyList; - - beforeEach(() => { - dummyConfig = { - endpoint: 'dummy endpoint', - searchKey: 'dummy search key', - }; - dummyList = { - data: [], - list: document.createElement('div'), - }; - - AjaxFilter.hook = { - config: { - AjaxFilter: dummyConfig, - }, - list: dummyList, - }; - }); - - describe('trigger', () => { - let ajaxSpy; - - beforeEach(() => { - spyOn(AjaxCache, 'retrieve').and.callFake(url => ajaxSpy(url)); - spyOn(AjaxFilter, '_loadData'); - - dummyConfig.onLoadingFinished = jasmine.createSpy('spy'); - - const dynamicList = document.createElement('div'); - dynamicList.dataset.dynamic = true; - dummyList.list.appendChild(dynamicList); - }); - - it('calls onLoadingFinished after loading data', done => { - ajaxSpy = url => { - expect(url).toBe('dummy endpoint?dummy search key='); - return Promise.resolve(dummyData); - }; - - AjaxFilter.trigger() - .then(() => { - expect(dummyConfig.onLoadingFinished.calls.count()).toBe(1); - }) - .then(done) - .catch(done.fail); - }); - - it('does not call onLoadingFinished if Ajax call fails', done => { - const dummyError = new Error('My dummy is sick! :-('); - ajaxSpy = url => { - expect(url).toBe('dummy endpoint?dummy search key='); - return Promise.reject(dummyError); - }; - - AjaxFilter.trigger() - .then(done.fail) - .catch(error => { - expect(error).toBe(dummyError); - expect(dummyConfig.onLoadingFinished.calls.count()).toBe(0); - }) - .then(done) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/droplab/plugins/ajax_spec.js b/spec/javascripts/droplab/plugins/ajax_spec.js deleted file mode 100644 index 2f492d00c0a..00000000000 --- a/spec/javascripts/droplab/plugins/ajax_spec.js +++ /dev/null @@ -1,41 +0,0 @@ -import AjaxCache from '~/lib/utils/ajax_cache'; -import Ajax from '~/droplab/plugins/ajax'; - -describe('Ajax', () => { - describe('preprocessing', () => { - const config = {}; - - describe('is not configured', () => { - it('passes the data through', () => { - const data = ['data']; - - expect(Ajax.preprocessing(config, data)).toEqual(data); - }); - }); - - describe('is configured', () => { - const processedArray = ['processed']; - - beforeEach(() => { - config.preprocessing = () => processedArray; - spyOn(config, 'preprocessing').and.callFake(() => processedArray); - }); - - it('calls preprocessing', () => { - Ajax.preprocessing(config, []); - - expect(config.preprocessing.calls.count()).toBe(1); - }); - - it('overrides AjaxCache', () => { - spyOn(AjaxCache, 'override').and.callFake((endpoint, results) => { - expect(results).toEqual(processedArray); - }); - - Ajax.preprocessing(config, []); - - expect(AjaxCache.override.calls.count()).toBe(1); - }); - }); - }); -}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js deleted file mode 100644 index 7f9425d8abe..00000000000 --- a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js +++ /dev/null @@ -1,30 +0,0 @@ -import domContentLoaded from '~/feature_highlight/feature_highlight_options'; -import bp from '~/breakpoints'; - -describe('feature highlight options', () => { - describe('domContentLoaded', () => { - it('should not call highlightFeatures when breakpoint is xs', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('xs'); - - expect(domContentLoaded()).toBe(false); - }); - - it('should not call highlightFeatures when breakpoint is sm', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('sm'); - - expect(domContentLoaded()).toBe(false); - }); - - it('should not call highlightFeatures when breakpoint is md', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('md'); - - expect(domContentLoaded()).toBe(false); - }); - - it('should call highlightFeatures when breakpoint is lg', () => { - spyOn(bp, 'getBreakpointSize').and.returnValue('lg'); - - expect(domContentLoaded()).toBe(true); - }); - }); -}); diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js deleted file mode 100644 index d1742dcedfa..00000000000 --- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js +++ /dev/null @@ -1,201 +0,0 @@ -import Vue from 'vue'; -import eventHub from '~/filtered_search/event_hub'; -import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue'; -import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; - -const createComponent = propsData => { - const Component = Vue.extend(RecentSearchesDropdownContent); - - return new Component({ - el: document.createElement('div'), - propsData, - }); -}; - -// Remove all the newlines and whitespace from the formatted markup -const trimMarkupWhitespace = text => text.replace(/(\n|\s)+/gm, ' ').trim(); - -describe('RecentSearchesDropdownContent', () => { - const propsDataWithoutItems = { - items: [], - allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(), - }; - const propsDataWithItems = { - items: ['foo', 'author:@root label:~foo bar'], - allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(), - }; - - let vm; - afterEach(() => { - if (vm) { - vm.$destroy(); - } - }); - - describe('with no items', () => { - let el; - - beforeEach(() => { - vm = createComponent(propsDataWithoutItems); - el = vm.$el; - }); - - it('should render empty state', () => { - expect(el.querySelector('.dropdown-info-note')).toBeDefined(); - - const items = el.querySelectorAll('.filtered-search-history-dropdown-item'); - - expect(items.length).toEqual(propsDataWithoutItems.items.length); - }); - }); - - describe('with items', () => { - let el; - - beforeEach(() => { - vm = createComponent(propsDataWithItems); - el = vm.$el; - }); - - it('should render clear recent searches button', () => { - expect(el.querySelector('.filtered-search-history-clear-button')).toBeDefined(); - }); - - it('should render recent search items', () => { - const items = el.querySelectorAll('.filtered-search-history-dropdown-item'); - - expect(items.length).toEqual(propsDataWithItems.items.length); - - expect( - trimMarkupWhitespace( - items[0].querySelector('.filtered-search-history-dropdown-search-token').textContent, - ), - ).toEqual('foo'); - - const item1Tokens = items[1].querySelectorAll('.filtered-search-history-dropdown-token'); - - expect(item1Tokens.length).toEqual(2); - expect(item1Tokens[0].querySelector('.name').textContent).toEqual('author:'); - expect(item1Tokens[0].querySelector('.value').textContent).toEqual('@root'); - expect(item1Tokens[1].querySelector('.name').textContent).toEqual('label:'); - expect(item1Tokens[1].querySelector('.value').textContent).toEqual('~foo'); - expect( - trimMarkupWhitespace( - items[1].querySelector('.filtered-search-history-dropdown-search-token').textContent, - ), - ).toEqual('bar'); - }); - }); - - describe('if isLocalStorageAvailable is `false`', () => { - let el; - - beforeEach(() => { - const props = Object.assign({ isLocalStorageAvailable: false }, propsDataWithItems); - - vm = createComponent(props); - el = vm.$el; - }); - - it('should render an info note', () => { - const note = el.querySelector('.dropdown-info-note'); - const items = el.querySelectorAll('.filtered-search-history-dropdown-item'); - - expect(note).toBeDefined(); - expect(note.innerText.trim()).toBe('This feature requires local storage to be enabled'); - expect(items.length).toEqual(propsDataWithoutItems.items.length); - }); - }); - - describe('computed', () => { - describe('processedItems', () => { - it('with items', () => { - vm = createComponent(propsDataWithItems); - const { processedItems } = vm; - - expect(processedItems.length).toEqual(2); - - expect(processedItems[0].text).toEqual(propsDataWithItems.items[0]); - expect(processedItems[0].tokens).toEqual([]); - expect(processedItems[0].searchToken).toEqual('foo'); - - expect(processedItems[1].text).toEqual(propsDataWithItems.items[1]); - expect(processedItems[1].tokens.length).toEqual(2); - expect(processedItems[1].tokens[0].prefix).toEqual('author:'); - expect(processedItems[1].tokens[0].suffix).toEqual('@root'); - expect(processedItems[1].tokens[1].prefix).toEqual('label:'); - expect(processedItems[1].tokens[1].suffix).toEqual('~foo'); - expect(processedItems[1].searchToken).toEqual('bar'); - }); - - it('with no items', () => { - vm = createComponent(propsDataWithoutItems); - const { processedItems } = vm; - - expect(processedItems.length).toEqual(0); - }); - }); - - describe('hasItems', () => { - it('with items', () => { - vm = createComponent(propsDataWithItems); - const { hasItems } = vm; - - expect(hasItems).toEqual(true); - }); - - it('with no items', () => { - vm = createComponent(propsDataWithoutItems); - const { hasItems } = vm; - - expect(hasItems).toEqual(false); - }); - }); - }); - - describe('methods', () => { - describe('onItemActivated', () => { - let onRecentSearchesItemSelectedSpy; - - beforeEach(() => { - onRecentSearchesItemSelectedSpy = jasmine.createSpy('spy'); - eventHub.$on('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy); - - vm = createComponent(propsDataWithItems); - }); - - afterEach(() => { - eventHub.$off('recentSearchesItemSelected', onRecentSearchesItemSelectedSpy); - }); - - it('emits event', () => { - expect(onRecentSearchesItemSelectedSpy).not.toHaveBeenCalled(); - vm.onItemActivated('something'); - - expect(onRecentSearchesItemSelectedSpy).toHaveBeenCalledWith('something'); - }); - }); - - describe('onRequestClearRecentSearches', () => { - let onRequestClearRecentSearchesSpy; - - beforeEach(() => { - onRequestClearRecentSearchesSpy = jasmine.createSpy('spy'); - eventHub.$on('requestClearRecentSearches', onRequestClearRecentSearchesSpy); - - vm = createComponent(propsDataWithItems); - }); - - afterEach(() => { - eventHub.$off('requestClearRecentSearches', onRequestClearRecentSearchesSpy); - }); - - it('emits event', () => { - expect(onRequestClearRecentSearchesSpy).not.toHaveBeenCalled(); - vm.onRequestClearRecentSearches({ stopPropagation: () => {} }); - - expect(onRequestClearRecentSearchesSpy).toHaveBeenCalled(); - }); - }); - }); -}); diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js deleted file mode 100644 index f764800fff0..00000000000 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ /dev/null @@ -1,113 +0,0 @@ -import DropdownUtils from '~/filtered_search/dropdown_utils'; -import DropdownUser from '~/filtered_search/dropdown_user'; -import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer'; -import IssuableFilteredTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; - -describe('Dropdown User', () => { - describe('getSearchInput', () => { - let dropdownUser; - - beforeEach(() => { - spyOn(DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); - spyOn(DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); - spyOn(DropdownUtils, 'getSearchInput').and.callFake(() => {}); - - dropdownUser = new DropdownUser({ - tokenKeys: IssuableFilteredTokenKeys, - }); - }); - - it('should not return the double quote found in value', () => { - spyOn(FilteredSearchTokenizer, 'processTokens').and.returnValue({ - lastToken: '"johnny appleseed', - }); - - expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); - }); - - it('should not return the single quote found in value', () => { - spyOn(FilteredSearchTokenizer, 'processTokens').and.returnValue({ - lastToken: "'larry boy", - }); - - expect(dropdownUser.getSearchInput()).toBe('larry boy'); - }); - }); - - describe("config AjaxFilter's endpoint", () => { - beforeEach(() => { - spyOn(DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); - spyOn(DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); - spyOn(DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); - }); - - it('should return endpoint', () => { - window.gon = { - relative_url_root: '', - }; - const dropdown = new DropdownUser(); - - expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); - }); - - it('should return endpoint when relative_url_root is undefined', () => { - const dropdown = new DropdownUser(); - - expect(dropdown.config.AjaxFilter.endpoint).toBe('/autocomplete/users.json'); - }); - - it('should return endpoint with relative url when available', () => { - window.gon = { - relative_url_root: '/gitlab_directory', - }; - const dropdown = new DropdownUser(); - - expect(dropdown.config.AjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); - }); - - afterEach(() => { - window.gon = {}; - }); - }); - - describe('hideCurrentUser', () => { - const fixtureTemplate = 'issues/issue_list.html'; - preloadFixtures(fixtureTemplate); - - let dropdown; - let authorFilterDropdownElement; - - beforeEach(() => { - loadFixtures(fixtureTemplate); - authorFilterDropdownElement = document.querySelector('#js-dropdown-author'); - const dummyInput = document.createElement('div'); - dropdown = new DropdownUser({ - dropdown: authorFilterDropdownElement, - input: dummyInput, - }); - }); - - const findCurrentUserElement = () => - authorFilterDropdownElement.querySelector('.js-current-user'); - - it('hides the current user from dropdown', () => { - const currentUserElement = findCurrentUserElement(); - - expect(currentUserElement).not.toBe(null); - - dropdown.hideCurrentUser(); - - expect(currentUserElement.classList).toContain('hidden'); - }); - - it('does nothing if no user is logged in', () => { - const currentUserElement = findCurrentUserElement(); - currentUserElement.parentNode.removeChild(currentUserElement); - - expect(findCurrentUserElement()).toBe(null); - - dropdown.hideCurrentUser(); - }); - }); -}); diff --git a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js deleted file mode 100644 index be11af8428f..00000000000 --- a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js +++ /dev/null @@ -1,83 +0,0 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue'; -import eventHub from '~/frequent_items/event_hub'; - -const localVue = createLocalVue(); - -const createComponent = (namespace = 'projects') => - shallowMount(localVue.extend(searchComponent), { - propsData: { namespace }, - localVue, - sync: false, - }); - -describe('FrequentItemsSearchInputComponent', () => { - let wrapper; - let vm; - - beforeEach(() => { - wrapper = createComponent(); - - ({ vm } = wrapper); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('methods', () => { - describe('setFocus', () => { - it('should set focus to search input', () => { - spyOn(vm.$refs.search, 'focus'); - - vm.setFocus(); - - expect(vm.$refs.search.focus).toHaveBeenCalled(); - }); - }); - }); - - describe('mounted', () => { - it('should listen `dropdownOpen` event', done => { - spyOn(eventHub, '$on'); - const vmX = createComponent().vm; - - localVue.nextTick(() => { - expect(eventHub.$on).toHaveBeenCalledWith( - `${vmX.namespace}-dropdownOpen`, - jasmine.any(Function), - ); - done(); - }); - }); - }); - - describe('beforeDestroy', () => { - it('should unbind event listeners on eventHub', done => { - const vmX = createComponent().vm; - spyOn(eventHub, '$off'); - - vmX.$mount(); - vmX.$destroy(); - - localVue.nextTick(() => { - expect(eventHub.$off).toHaveBeenCalledWith( - `${vmX.namespace}-dropdownOpen`, - jasmine.any(Function), - ); - done(); - }); - }); - }); - - describe('template', () => { - it('should render component element', () => { - expect(wrapper.classes()).toContain('search-input-container'); - expect(wrapper.contains('input.form-control')).toBe(true); - expect(wrapper.contains('.search-icon')).toBe(true); - expect(wrapper.find('input.form-control').attributes('placeholder')).toBe( - 'Search your projects', - ); - }); - }); -}); diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js deleted file mode 100644 index 294f219d6fe..00000000000 --- a/spec/javascripts/gl_field_errors_spec.js +++ /dev/null @@ -1,138 +0,0 @@ -/* eslint-disable arrow-body-style */ - -import $ from 'jquery'; -import GlFieldErrors from '~/gl_field_errors'; - -describe('GL Style Field Errors', function() { - preloadFixtures('static/gl_field_errors.html'); - - beforeEach(function() { - loadFixtures('static/gl_field_errors.html'); - const $form = $('form.gl-show-field-errors'); - - this.$form = $form; - this.fieldErrors = new GlFieldErrors($form); - }); - - it('should select the correct input elements', function() { - expect(this.$form).toBeDefined(); - expect(this.$form.length).toBe(1); - expect(this.fieldErrors).toBeDefined(); - const { inputs } = this.fieldErrors.state; - - expect(inputs.length).toBe(4); - }); - - it('should ignore elements with custom error handling', function() { - const customErrorFlag = 'gl-field-error-ignore'; - const customErrorElem = $(`.${customErrorFlag}`); - - expect(customErrorElem.length).toBe(1); - - const customErrors = this.fieldErrors.state.inputs.filter(input => { - return input.inputElement.hasClass(customErrorFlag); - }); - - expect(customErrors.length).toBe(0); - }); - - it('should not show any errors before submit attempt', function() { - this.$form - .find('.email') - .val('not-a-valid-email') - .keyup(); - this.$form - .find('.text-required') - .val('') - .keyup(); - this.$form - .find('.alphanumberic') - .val('?---*') - .keyup(); - - const errorsShown = this.$form.find('.gl-field-error-outline'); - - expect(errorsShown.length).toBe(0); - }); - - it('should show errors when input valid is submitted', function() { - this.$form - .find('.email') - .val('not-a-valid-email') - .keyup(); - this.$form - .find('.text-required') - .val('') - .keyup(); - this.$form - .find('.alphanumberic') - .val('?---*') - .keyup(); - - this.$form.submit(); - - const errorsShown = this.$form.find('.gl-field-error-outline'); - - expect(errorsShown.length).toBe(4); - }); - - it('should properly track validity state on input after invalid submission attempt', function() { - this.$form.submit(); - - const emailInputModel = this.fieldErrors.state.inputs[1]; - const fieldState = emailInputModel.state; - const emailInputElement = emailInputModel.inputElement; - - // No input - expect(emailInputElement).toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(true); - expect(fieldState.valid).toBe(false); - - // Then invalid input - emailInputElement.val('not-a-valid-email').keyup(); - - expect(emailInputElement).toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(false); - expect(fieldState.valid).toBe(false); - - // Then valid input - emailInputElement.val('email@gitlab.com').keyup(); - - expect(emailInputElement).not.toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(false); - expect(fieldState.valid).toBe(true); - - // Then invalid input - emailInputElement.val('not-a-valid-email').keyup(); - - expect(emailInputElement).toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(false); - expect(fieldState.valid).toBe(false); - - // Then empty input - emailInputElement.val('').keyup(); - - expect(emailInputElement).toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(true); - expect(fieldState.valid).toBe(false); - - // Then valid input - emailInputElement.val('email@gitlab.com').keyup(); - - expect(emailInputElement).not.toHaveClass('gl-field-error-outline'); - expect(fieldState.empty).toBe(false); - expect(fieldState.valid).toBe(true); - }); - - it('should properly infer error messages', function() { - this.$form.submit(); - const trackedInputs = this.fieldErrors.state.inputs; - const inputHasTitle = trackedInputs[1]; - const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error'); - const inputNoTitle = trackedInputs[2]; - const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error'); - - expect(noTitleErrorElem.text()).toBe('This field is required.'); - expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.'); - }); -}); diff --git a/spec/javascripts/gpg_badges_spec.js b/spec/javascripts/gpg_badges_spec.js deleted file mode 100644 index 4731484e02d..00000000000 --- a/spec/javascripts/gpg_badges_spec.js +++ /dev/null @@ -1,92 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { TEST_HOST } from 'spec/test_constants'; -import axios from '~/lib/utils/axios_utils'; -import GpgBadges from '~/gpg_badges'; - -describe('GpgBadges', () => { - let mock; - const dummyCommitSha = 'n0m0rec0ffee'; - const dummyBadgeHtml = 'dummy html'; - const dummyResponse = { - signatures: [ - { - commit_sha: dummyCommitSha, - html: dummyBadgeHtml, - }, - ], - }; - const dummyUrl = `${TEST_HOST}/dummy/signatures`; - - beforeEach(() => { - mock = new MockAdapter(axios); - setFixtures(` -
- - -
-
-
-
- `); - }); - - afterEach(() => { - mock.restore(); - }); - - it('does not make a request if there is no container element', done => { - setFixtures(''); - spyOn(axios, 'get'); - - GpgBadges.fetch() - .then(() => { - expect(axios.get).not.toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('throws an error if the endpoint is missing', done => { - setFixtures('
'); - spyOn(axios, 'get'); - - GpgBadges.fetch() - .then(() => done.fail('Expected error to be thrown')) - .catch(error => { - expect(error.message).toBe('Missing commit signatures endpoint!'); - expect(axios.get).not.toHaveBeenCalled(); - }) - .then(done) - .catch(done.fail); - }); - - it('displays a loading spinner', done => { - mock.onGet(dummyUrl).replyOnce(200); - - GpgBadges.fetch() - .then(() => { - expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null); - const spinners = document.querySelectorAll('.js-loading-gpg-badge i.fa.fa-spinner.fa-spin'); - - expect(spinners.length).toBe(1); - done(); - }) - .catch(done.fail); - }); - - it('replaces the loading spinner', done => { - mock.onGet(dummyUrl).replyOnce(200, dummyResponse); - - GpgBadges.fetch() - .then(() => { - expect(document.querySelector('.js-loading-gpg-badge')).toBe(null); - const parentContainer = document.querySelector('.parent-container'); - - expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml); - done(); - }) - .catch(done.fail); - }); -}); diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js deleted file mode 100644 index c36d3be1b22..00000000000 --- a/spec/javascripts/header_spec.js +++ /dev/null @@ -1,53 +0,0 @@ -import $ from 'jquery'; -import initTodoToggle from '~/header'; - -describe('Header', function() { - const todosPendingCount = '.todos-count'; - const fixtureTemplate = 'issues/open-issue.html'; - - function isTodosCountHidden() { - return $(todosPendingCount).hasClass('hidden'); - } - - function triggerToggle(newCount) { - $(document).trigger('todo:toggle', newCount); - } - - preloadFixtures(fixtureTemplate); - beforeEach(() => { - initTodoToggle(); - loadFixtures(fixtureTemplate); - }); - - it('should update todos-count after receiving the todo:toggle event', () => { - triggerToggle(5); - - expect($(todosPendingCount).text()).toEqual('5'); - }); - - it('should hide todos-count when it is 0', () => { - triggerToggle(0); - - expect(isTodosCountHidden()).toEqual(true); - }); - - it('should show todos-count when it is more than 0', () => { - triggerToggle(10); - - expect(isTodosCountHidden()).toEqual(false); - }); - - describe('when todos-count is 1000', () => { - beforeEach(() => { - triggerToggle(1000); - }); - - it('should show todos-count', () => { - expect(isTodosCountHidden()).toEqual(false); - }); - - it('should show 99+ for todos-count', () => { - expect($(todosPendingCount).text()).toEqual('99+'); - }); - }); -}); diff --git a/spec/javascripts/helpers/class_spec_helper_spec.js b/spec/javascripts/helpers/class_spec_helper_spec.js deleted file mode 100644 index f6268b0fb6d..00000000000 --- a/spec/javascripts/helpers/class_spec_helper_spec.js +++ /dev/null @@ -1,20 +0,0 @@ -/* global ClassSpecHelper */ - -import './class_spec_helper'; - -describe('ClassSpecHelper', function() { - describe('itShouldBeAStaticMethod', () => { - beforeEach(() => { - class TestClass { - instanceMethod() { - this.prop = 'val'; - } - static staticMethod() {} - } - - this.TestClass = TestClass; - }); - - ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod'); - }); -}); diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js deleted file mode 100644 index e09ccbe2a63..00000000000 --- a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js +++ /dev/null @@ -1,46 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import stageButton from '~/ide/components/commit_sidebar/stage_button.vue'; -import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; -import { file, resetStore } from '../../helpers'; - -describe('IDE stage file button', () => { - let vm; - let f; - - beforeEach(() => { - const Component = Vue.extend(stageButton); - f = file(); - - vm = createComponentWithStore(Component, store, { - path: f.path, - }); - - spyOn(vm, 'stageChange'); - spyOn(vm, 'discardFileChanges'); - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders button to discard & stage', () => { - expect(vm.$el.querySelectorAll('.btn-blank').length).toBe(2); - }); - - it('calls store with stage button', () => { - vm.$el.querySelectorAll('.btn')[0].click(); - - expect(vm.stageChange).toHaveBeenCalledWith(f.path); - }); - - it('calls store with discard button', () => { - vm.$el.querySelector('.btn-danger').click(); - - expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path); - }); -}); diff --git a/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js deleted file mode 100644 index 917bbb9fb46..00000000000 --- a/spec/javascripts/ide/components/commit_sidebar/unstage_button_spec.js +++ /dev/null @@ -1,39 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import unstageButton from '~/ide/components/commit_sidebar/unstage_button.vue'; -import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper'; -import { file, resetStore } from '../../helpers'; - -describe('IDE unstage file button', () => { - let vm; - let f; - - beforeEach(() => { - const Component = Vue.extend(unstageButton); - f = file(); - - vm = createComponentWithStore(Component, store, { - path: f.path, - }); - - spyOn(vm, 'unstageChange'); - - vm.$mount(); - }); - - afterEach(() => { - vm.$destroy(); - - resetStore(vm.$store); - }); - - it('renders button to unstage', () => { - expect(vm.$el.querySelectorAll('.btn').length).toBe(1); - }); - - it('calls store with unnstage button', () => { - vm.$el.querySelector('.btn').click(); - - expect(vm.unstageChange).toHaveBeenCalledWith(f.path); - }); -}); diff --git a/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js b/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js deleted file mode 100644 index fff382a107f..00000000000 --- a/spec/javascripts/ide/components/jobs/detail/scroll_button_spec.js +++ /dev/null @@ -1,59 +0,0 @@ -import Vue from 'vue'; -import ScrollButton from '~/ide/components/jobs/detail/scroll_button.vue'; -import mountComponent from '../../../../helpers/vue_mount_component_helper'; - -describe('IDE job log scroll button', () => { - const Component = Vue.extend(ScrollButton); - let vm; - - beforeEach(() => { - vm = mountComponent(Component, { - direction: 'up', - disabled: false, - }); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('iconName', () => { - ['up', 'down'].forEach(direction => { - it(`returns icon name for ${direction}`, () => { - vm.direction = direction; - - expect(vm.iconName).toBe(`scroll_${direction}`); - }); - }); - }); - - describe('tooltipTitle', () => { - it('returns title for up', () => { - expect(vm.tooltipTitle).toBe('Scroll to top'); - }); - - it('returns title for down', () => { - vm.direction = 'down'; - - expect(vm.tooltipTitle).toBe('Scroll to bottom'); - }); - }); - - it('emits click event on click', () => { - spyOn(vm, '$emit'); - - vm.$el.querySelector('.btn-scroll').click(); - - expect(vm.$emit).toHaveBeenCalledWith('click'); - }); - - it('disables button when disabled is true', done => { - vm.disabled = true; - - vm.$nextTick(() => { - expect(vm.$el.querySelector('.btn-scroll').hasAttribute('disabled')).toBe(true); - - done(); - }); - }); -}); diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js deleted file mode 100644 index 03d1125c23a..00000000000 --- a/spec/javascripts/ide/stores/actions/file_spec.js +++ /dev/null @@ -1,805 +0,0 @@ -import Vue from 'vue'; -import MockAdapter from 'axios-mock-adapter'; -import axios from '~/lib/utils/axios_utils'; -import store from '~/ide/stores'; -import * as actions from '~/ide/stores/actions/file'; -import * as types from '~/ide/stores/mutation_types'; -import service from '~/ide/services'; -import router from '~/ide/ide_router'; -import eventHub from '~/ide/eventhub'; -import { file, resetStore } from '../../helpers'; -import testAction from '../../../helpers/vuex_action_helper'; - -const RELATIVE_URL_ROOT = '/gitlab'; - -describe('IDE store file actions', () => { - let mock; - let originalGon; - - beforeEach(() => { - mock = new MockAdapter(axios); - originalGon = window.gon; - window.gon = { - ...window.gon, - relative_url_root: RELATIVE_URL_ROOT, - }; - - spyOn(router, 'push'); - }); - - afterEach(() => { - mock.restore(); - resetStore(store); - window.gon = originalGon; - }); - - describe('closeFile', () => { - let localFile; - - beforeEach(() => { - localFile = file('testFile'); - localFile.active = true; - localFile.opened = true; - localFile.parentTreeUrl = 'parentTreeUrl'; - - store.state.openFiles.push(localFile); - store.state.entries[localFile.path] = localFile; - }); - - it('closes open files', done => { - store - .dispatch('closeFile', localFile) - .then(() => { - expect(localFile.opened).toBeFalsy(); - expect(localFile.active).toBeFalsy(); - expect(store.state.openFiles.length).toBe(0); - - done(); - }) - .catch(done.fail); - }); - - it('closes file even if file has changes', done => { - store.state.changedFiles.push(localFile); - - store - .dispatch('closeFile', localFile) - .then(Vue.nextTick) - .then(() => { - expect(store.state.openFiles.length).toBe(0); - expect(store.state.changedFiles.length).toBe(1); - - done(); - }) - .catch(done.fail); - }); - - it('closes file & opens next available file', done => { - const f = { - ...file('newOpenFile'), - url: '/newOpenFile', - }; - - store.state.openFiles.push(f); - store.state.entries[f.path] = f; - - store - .dispatch('closeFile', localFile) - .then(Vue.nextTick) - .then(() => { - expect(router.push).toHaveBeenCalledWith(`/project${f.url}`); - - done(); - }) - .catch(done.fail); - }); - - it('removes file if it pending', done => { - store.state.openFiles.push({ - ...localFile, - pending: true, - }); - - store - .dispatch('closeFile', localFile) - .then(() => { - expect(store.state.openFiles.length).toBe(0); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('setFileActive', () => { - let localFile; - let scrollToTabSpy; - let oldScrollToTab; - - beforeEach(() => { - scrollToTabSpy = jasmine.createSpy('scrollToTab'); - oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line - store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line - - localFile = file('setThisActive'); - - store.state.entries[localFile.path] = localFile; - }); - - afterEach(() => { - store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line - }); - - it('calls scrollToTab', () => { - const dispatch = jasmine.createSpy(); - - actions.setFileActive( - { commit() {}, state: store.state, getters: store.getters, dispatch }, - localFile.path, - ); - - expect(dispatch).toHaveBeenCalledWith('scrollToTab'); - }); - - it('commits SET_FILE_ACTIVE', () => { - const commit = jasmine.createSpy(); - - actions.setFileActive( - { commit, state: store.state, getters: store.getters, dispatch() {} }, - localFile.path, - ); - - expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', { - path: localFile.path, - active: true, - }); - }); - - it('sets current active file to not active', () => { - const f = file('newActive'); - store.state.entries[f.path] = f; - localFile.active = true; - store.state.openFiles.push(localFile); - - const commit = jasmine.createSpy(); - - actions.setFileActive( - { commit, state: store.state, getters: store.getters, dispatch() {} }, - f.path, - ); - - expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', { - path: localFile.path, - active: false, - }); - }); - }); - - describe('getFileData', () => { - let localFile; - - beforeEach(() => { - spyOn(service, 'getFileData').and.callThrough(); - - localFile = file(`newCreate-${Math.random()}`); - store.state.entries[localFile.path] = localFile; - - store.state.currentProjectId = 'test/test'; - store.state.currentBranchId = 'master'; - - store.state.projects['test/test'] = { - branches: { - master: { - commit: { - id: '7297abc', - }, - }, - }, - }; - }); - - describe('success', () => { - beforeEach(() => { - mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`).replyOnce( - 200, - { - blame_path: 'blame_path', - commits_path: 'commits_path', - permalink: 'permalink', - raw_path: 'raw_path', - binary: false, - html: '123', - render_error: '', - }, - { - 'page-title': 'testing getFileData', - }, - ); - }); - - it('calls the service', done => { - store - .dispatch('getFileData', { path: localFile.path }) - .then(() => { - expect(service.getFileData).toHaveBeenCalledWith( - `${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`, - ); - - done(); - }) - .catch(done.fail); - }); - - it('sets the file data', done => { - store - .dispatch('getFileData', { path: localFile.path }) - .then(() => { - expect(localFile.blamePath).toBe('blame_path'); - - done(); - }) - .catch(done.fail); - }); - - it('sets document title with the branchId', done => { - store - .dispatch('getFileData', { path: localFile.path }) - .then(() => { - expect(document.title).toBe(`${localFile.path} · master · test/test · GitLab`); - done(); - }) - .catch(done.fail); - }); - - it('sets the file as active', done => { - store - .dispatch('getFileData', { path: localFile.path }) - .then(() => { - expect(localFile.active).toBeTruthy(); - - done(); - }) - .catch(done.fail); - }); - - it('sets the file not as active if we pass makeFileActive false', done => { - store - .dispatch('getFileData', { path: localFile.path, makeFileActive: false }) - .then(() => { - expect(localFile.active).toBeFalsy(); - - done(); - }) - .catch(done.fail); - }); - - it('adds the file to open files', done => { - store - .dispatch('getFileData', { path: localFile.path }) - .then(() => { - expect(store.state.openFiles.length).toBe(1); - expect(store.state.openFiles[0].name).toBe(localFile.name); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('Re-named success', () => { - beforeEach(() => { - localFile = file(`newCreate-${Math.random()}`); - localFile.url = `project/getFileDataURL`; - localFile.prevPath = 'old-dull-file'; - localFile.path = 'new-shiny-file'; - store.state.entries[localFile.path] = localFile; - - mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/old-dull-file`).replyOnce( - 200, - { - blame_path: 'blame_path', - commits_path: 'commits_path', - permalink: 'permalink', - raw_path: 'raw_path', - binary: false, - html: '123', - render_error: '', - }, - { - 'page-title': 'testing old-dull-file', - }, - ); - }); - - it('sets document title considering `prevPath` on a file', done => { - store - .dispatch('getFileData', { path: localFile.path }) - .then(() => { - expect(document.title).toBe(`new-shiny-file · master · test/test · GitLab`); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock.onGet(`${RELATIVE_URL_ROOT}/test/test/7297abc/${localFile.path}`).networkError(); - }); - - it('dispatches error action', done => { - const dispatch = jasmine.createSpy('dispatch'); - - actions - .getFileData( - { state: store.state, commit() {}, dispatch, getters: store.getters }, - { path: localFile.path }, - ) - .then(() => { - expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occurred whilst loading the file.', - action: jasmine.any(Function), - actionText: 'Please try again', - actionPayload: { - path: localFile.path, - makeFileActive: true, - }, - }); - - done(); - }) - .catch(done.fail); - }); - }); - }); - - describe('getRawFileData', () => { - let tmpFile; - - beforeEach(() => { - spyOn(service, 'getRawFileData').and.callThrough(); - - tmpFile = file('tmpFile'); - store.state.entries[tmpFile.path] = tmpFile; - }); - - describe('success', () => { - beforeEach(() => { - mock.onGet(/(.*)/).replyOnce(200, 'raw'); - }); - - it('calls getRawFileData service method', done => { - store - .dispatch('getRawFileData', { path: tmpFile.path }) - .then(() => { - expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile); - - done(); - }) - .catch(done.fail); - }); - - it('updates file raw data', done => { - store - .dispatch('getRawFileData', { path: tmpFile.path }) - .then(() => { - expect(tmpFile.raw).toBe('raw'); - - done(); - }) - .catch(done.fail); - }); - - it('calls also getBaseRawFileData service method', done => { - spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw')); - - store.state.currentProjectId = 'gitlab-org/gitlab-ce'; - store.state.currentMergeRequestId = '1'; - store.state.projects = { - 'gitlab-org/gitlab-ce': { - mergeRequests: { - 1: { - baseCommitSha: 'SHA', - }, - }, - }, - }; - - tmpFile.mrChange = { new_file: false }; - - store - .dispatch('getRawFileData', { path: tmpFile.path }) - .then(() => { - expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA'); - expect(tmpFile.baseRaw).toBe('baseraw'); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('return JSON', () => { - beforeEach(() => { - mock.onGet(/(.*)/).replyOnce(200, JSON.stringify({ test: '123' })); - }); - - it('does not parse returned JSON', done => { - store - .dispatch('getRawFileData', { path: tmpFile.path }) - .then(() => { - expect(tmpFile.raw).toEqual('{"test":"123"}'); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('error', () => { - beforeEach(() => { - mock.onGet(/(.*)/).networkError(); - }); - - it('dispatches error action', done => { - const dispatch = jasmine.createSpy('dispatch'); - - actions - .getRawFileData({ state: store.state, commit() {}, dispatch }, { path: tmpFile.path }) - .then(done.fail) - .catch(() => { - expect(dispatch).toHaveBeenCalledWith('setErrorMessage', { - text: 'An error occurred whilst loading the file content.', - action: jasmine.any(Function), - actionText: 'Please try again', - actionPayload: { - path: tmpFile.path, - }, - }); - - done(); - }); - }); - }); - }); - - describe('changeFileContent', () => { - let tmpFile; - - beforeEach(() => { - tmpFile = file('tmpFile'); - tmpFile.content = '\n'; - tmpFile.raw = '\n'; - store.state.entries[tmpFile.path] = tmpFile; - }); - - it('updates file content', done => { - store - .dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content\n', - }) - .then(() => { - expect(tmpFile.content).toBe('content\n'); - - done(); - }) - .catch(done.fail); - }); - - it('adds a newline to the end of the file if it doesnt already exist', done => { - store - .dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content', - }) - .then(() => { - expect(tmpFile.content).toBe('content\n'); - - done(); - }) - .catch(done.fail); - }); - - it('adds file into changedFiles array', done => { - store - .dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content', - }) - .then(() => { - expect(store.state.changedFiles.length).toBe(1); - - done(); - }) - .catch(done.fail); - }); - - it('adds file once into changedFiles array', done => { - store - .dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content', - }) - .then(() => - store.dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content 123', - }), - ) - .then(() => { - expect(store.state.changedFiles.length).toBe(1); - - done(); - }) - .catch(done.fail); - }); - - it('removes file from changedFiles array if not changed', done => { - store - .dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content\n', - }) - .then(() => - store.dispatch('changeFileContent', { - path: tmpFile.path, - content: '\n', - }), - ) - .then(() => { - expect(store.state.changedFiles.length).toBe(0); - - done(); - }) - .catch(done.fail); - }); - - it('bursts unused seal', done => { - store - .dispatch('changeFileContent', { - path: tmpFile.path, - content: 'content', - }) - .then(() => { - expect(store.state.unusedSeal).toBe(false); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('discardFileChanges', () => { - let tmpFile; - - beforeEach(() => { - spyOn(eventHub, '$on'); - spyOn(eventHub, '$emit'); - - tmpFile = file(); - tmpFile.content = 'testing'; - - store.state.changedFiles.push(tmpFile); - store.state.entries[tmpFile.path] = tmpFile; - }); - - it('resets file content', done => { - store - .dispatch('discardFileChanges', tmpFile.path) - .then(() => { - expect(tmpFile.content).not.toBe('testing'); - - done(); - }) - .catch(done.fail); - }); - - it('removes file from changedFiles array', done => { - store - .dispatch('discardFileChanges', tmpFile.path) - .then(() => { - expect(store.state.changedFiles.length).toBe(0); - - done(); - }) - .catch(done.fail); - }); - - it('closes temp file', done => { - tmpFile.tempFile = true; - tmpFile.opened = true; - - store - .dispatch('discardFileChanges', tmpFile.path) - .then(() => { - expect(tmpFile.opened).toBeFalsy(); - - done(); - }) - .catch(done.fail); - }); - - it('does not re-open a closed temp file', done => { - tmpFile.tempFile = true; - - expect(tmpFile.opened).toBeFalsy(); - - store - .dispatch('discardFileChanges', tmpFile.path) - .then(() => { - expect(tmpFile.opened).toBeFalsy(); - - done(); - }) - .catch(done.fail); - }); - - it('pushes route for active file', done => { - tmpFile.active = true; - store.state.openFiles.push(tmpFile); - - store - .dispatch('discardFileChanges', tmpFile.path) - .then(() => { - expect(router.push).toHaveBeenCalledWith(`/project${tmpFile.url}`); - - done(); - }) - .catch(done.fail); - }); - - it('emits eventHub event to dispose cached model', done => { - store - .dispatch('discardFileChanges', tmpFile.path) - .then(() => { - expect(eventHub.$emit).toHaveBeenCalled(); - - done(); - }) - .catch(done.fail); - }); - }); - - describe('stageChange', () => { - it('calls STAGE_CHANGE with file path', done => { - testAction( - actions.stageChange, - 'path', - store.state, - [ - { type: types.STAGE_CHANGE, payload: 'path' }, - { type: types.SET_LAST_COMMIT_MSG, payload: '' }, - ], - [], - done, - ); - }); - }); - - describe('unstageChange', () => { - it('calls UNSTAGE_CHANGE with file path', done => { - testAction( - actions.unstageChange, - 'path', - store.state, - [{ type: types.UNSTAGE_CHANGE, payload: 'path' }], - [], - done, - ); - }); - }); - - describe('openPendingTab', () => { - let f; - - beforeEach(() => { - f = { - ...file(), - projectId: '123', - }; - - store.state.entries[f.path] = f; - }); - - it('makes file pending in openFiles', done => { - store - .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) - .then(() => { - expect(store.state.openFiles[0].pending).toBe(true); - }) - .then(done) - .catch(done.fail); - }); - - it('returns true when opened', done => { - store - .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) - .then(added => { - expect(added).toBe(true); - }) - .then(done) - .catch(done.fail); - }); - - it('returns false when already opened', done => { - store.state.openFiles.push({ - ...f, - active: true, - key: `pending-${f.key}`, - }); - - store - .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) - .then(added => { - expect(added).toBe(false); - }) - .then(done) - .catch(done.fail); - }); - - it('pushes router URL when added', done => { - store.state.currentBranchId = 'master'; - - store - .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' }) - .then(() => { - expect(router.push).toHaveBeenCalledWith('/project/123/tree/master/'); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('removePendingTab', () => { - let f; - - beforeEach(() => { - spyOn(eventHub, '$emit'); - - f = { - ...file('pendingFile'), - pending: true, - }; - }); - - it('removes pending file from open files', done => { - store.state.openFiles.push(f); - - store - .dispatch('removePendingTab', f) - .then(() => { - expect(store.state.openFiles.length).toBe(0); - }) - .then(done) - .catch(done.fail); - }); - - it('emits event to dispose model', done => { - store - .dispatch('removePendingTab', f) - .then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.${f.key}`); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('triggerFilesChange', () => { - beforeEach(() => { - spyOn(eventHub, '$emit'); - }); - - it('emits event that files have changed', done => { - store - .dispatch('triggerFilesChange') - .then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('ide.files.change'); - }) - .then(done) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/image_diff/helpers/init_image_diff_spec.js b/spec/javascripts/image_diff/helpers/init_image_diff_spec.js deleted file mode 100644 index ba501d58965..00000000000 --- a/spec/javascripts/image_diff/helpers/init_image_diff_spec.js +++ /dev/null @@ -1,52 +0,0 @@ -import initImageDiffHelper from '~/image_diff/helpers/init_image_diff'; -import ImageDiff from '~/image_diff/image_diff'; -import ReplacedImageDiff from '~/image_diff/replaced_image_diff'; - -describe('initImageDiff', () => { - let glCache; - let fileEl; - - beforeEach(() => { - window.gl = window.gl || (window.gl = {}); - glCache = window.gl; - fileEl = document.createElement('div'); - fileEl.innerHTML = ` -
- `; - - spyOn(ReplacedImageDiff.prototype, 'init').and.callFake(() => {}); - spyOn(ImageDiff.prototype, 'init').and.callFake(() => {}); - }); - - afterEach(() => { - window.gl = glCache; - }); - - it('should initialize ImageDiff if js-single-image', () => { - const diffFileEl = fileEl.querySelector('.diff-file'); - diffFileEl.innerHTML = ` -
-
- `; - - const imageDiff = initImageDiffHelper.initImageDiff(fileEl, true, false); - - expect(ImageDiff.prototype.init).toHaveBeenCalled(); - expect(imageDiff.canCreateNote).toEqual(true); - expect(imageDiff.renderCommentBadge).toEqual(false); - }); - - it('should initialize ReplacedImageDiff if js-replaced-image', () => { - const diffFileEl = fileEl.querySelector('.diff-file'); - diffFileEl.innerHTML = ` -
-
- `; - - const replacedImageDiff = initImageDiffHelper.initImageDiff(fileEl, false, true); - - expect(ReplacedImageDiff.prototype.init).toHaveBeenCalled(); - expect(replacedImageDiff.canCreateNote).toEqual(false); - expect(replacedImageDiff.renderCommentBadge).toEqual(true); - }); -}); diff --git a/spec/javascripts/image_diff/init_discussion_tab_spec.js b/spec/javascripts/image_diff/init_discussion_tab_spec.js deleted file mode 100644 index 5eb87e1df25..00000000000 --- a/spec/javascripts/image_diff/init_discussion_tab_spec.js +++ /dev/null @@ -1,40 +0,0 @@ -import initDiscussionTab from '~/image_diff/init_discussion_tab'; -import initImageDiffHelper from '~/image_diff/helpers/init_image_diff'; - -describe('initDiscussionTab', () => { - beforeEach(() => { - setFixtures(` -
-
-
-
- `); - }); - - it('should pass canCreateNote as false to initImageDiff', done => { - spyOn(initImageDiffHelper, 'initImageDiff').and.callFake((diffFileEl, canCreateNote) => { - expect(canCreateNote).toEqual(false); - done(); - }); - - initDiscussionTab(); - }); - - it('should pass renderCommentBadge as true to initImageDiff', done => { - spyOn(initImageDiffHelper, 'initImageDiff').and.callFake( - (diffFileEl, canCreateNote, renderCommentBadge) => { - expect(renderCommentBadge).toEqual(true); - done(); - }, - ); - - initDiscussionTab(); - }); - - it('should call initImageDiff for each diffFileEls', () => { - spyOn(initImageDiffHelper, 'initImageDiff').and.callFake(() => {}); - initDiscussionTab(); - - expect(initImageDiffHelper.initImageDiff.calls.count()).toEqual(2); - }); -}); diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js deleted file mode 100644 index 2ab74ae4e10..00000000000 --- a/spec/javascripts/issue_show/components/edit_actions_spec.js +++ /dev/null @@ -1,134 +0,0 @@ -import Vue from 'vue'; -import editActions from '~/issue_show/components/edit_actions.vue'; -import eventHub from '~/issue_show/event_hub'; -import Store from '~/issue_show/stores'; - -describe('Edit Actions components', () => { - let vm; - - beforeEach(done => { - const Component = Vue.extend(editActions); - const store = new Store({ - titleHtml: '', - descriptionHtml: '', - issuableRef: '', - }); - store.formState.title = 'test'; - - spyOn(eventHub, '$emit'); - - vm = new Component({ - propsData: { - canDestroy: true, - formState: store.formState, - issuableType: 'issue', - }, - }).$mount(); - - Vue.nextTick(done); - }); - - it('renders all buttons as enabled', () => { - expect(vm.$el.querySelectorAll('.disabled').length).toBe(0); - - expect(vm.$el.querySelectorAll('[disabled]').length).toBe(0); - }); - - it('does not render delete button if canUpdate is false', done => { - vm.canDestroy = false; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-danger')).toBeNull(); - - done(); - }); - }); - - it('disables submit button when title is blank', done => { - vm.formState.title = ''; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-success').getAttribute('disabled')).toBe('disabled'); - - done(); - }); - }); - - it('should not show delete button if showDeleteButton is false', done => { - vm.showDeleteButton = false; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-danger')).toBeNull(); - done(); - }); - }); - - describe('updateIssuable', () => { - it('sends update.issauble event when clicking save button', () => { - vm.$el.querySelector('.btn-success').click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable'); - }); - - it('shows loading icon after clicking save button', done => { - vm.$el.querySelector('.btn-success').click(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-success .fa')).not.toBeNull(); - - done(); - }); - }); - - it('disabled button after clicking save button', done => { - vm.$el.querySelector('.btn-success').click(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-success').getAttribute('disabled')).toBe('disabled'); - - done(); - }); - }); - }); - - describe('closeForm', () => { - it('emits close.form when clicking cancel', () => { - vm.$el.querySelector('.btn-default').click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('close.form'); - }); - }); - - describe('deleteIssuable', () => { - it('sends delete.issuable event when clicking save button', () => { - spyOn(window, 'confirm').and.returnValue(true); - vm.$el.querySelector('.btn-danger').click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('delete.issuable', { destroy_confirm: true }); - }); - - it('shows loading icon after clicking delete button', done => { - spyOn(window, 'confirm').and.returnValue(true); - vm.$el.querySelector('.btn-danger').click(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn-danger .fa')).not.toBeNull(); - - done(); - }); - }); - - it('does no actions when confirm is false', done => { - spyOn(window, 'confirm').and.returnValue(false); - vm.$el.querySelector('.btn-danger').click(); - - Vue.nextTick(() => { - expect(eventHub.$emit).not.toHaveBeenCalledWith('delete.issuable'); - - expect(vm.$el.querySelector('.btn-danger .fa')).toBeNull(); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js deleted file mode 100644 index f5f87a6bfbf..00000000000 --- a/spec/javascripts/issue_show/components/fields/description_spec.js +++ /dev/null @@ -1,70 +0,0 @@ -import Vue from 'vue'; -import eventHub from '~/issue_show/event_hub'; -import Store from '~/issue_show/stores'; -import descriptionField from '~/issue_show/components/fields/description.vue'; -import { keyboardDownEvent } from '../../helpers'; - -describe('Description field component', () => { - let vm; - let store; - - beforeEach(done => { - const Component = Vue.extend(descriptionField); - const el = document.createElement('div'); - store = new Store({ - titleHtml: '', - descriptionHtml: '', - issuableRef: '', - }); - store.formState.description = 'test'; - - document.body.appendChild(el); - - spyOn(eventHub, '$emit'); - - vm = new Component({ - el, - propsData: { - markdownPreviewPath: '/', - markdownDocsPath: '/', - formState: store.formState, - }, - }).$mount(); - - Vue.nextTick(done); - }); - - it('renders markdown field with description', () => { - expect(vm.$el.querySelector('.md-area textarea').value).toBe('test'); - }); - - it('renders markdown field with a markdown description', done => { - store.formState.description = '**test**'; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.md-area textarea').value).toBe('**test**'); - - done(); - }); - }); - - it('focuses field when mounted', () => { - expect(document.activeElement).toBe(vm.$refs.textarea); - }); - - it('triggers update with meta+enter', () => { - vm.$el.querySelector('.md-area textarea').dispatchEvent(keyboardDownEvent(13, true)); - - expect(eventHub.$emit).toHaveBeenCalled(); - }); - - it('triggers update with ctrl+enter', () => { - vm.$el.querySelector('.md-area textarea').dispatchEvent(keyboardDownEvent(13, false, true)); - - expect(eventHub.$emit).toHaveBeenCalled(); - }); - - it('has a ref named `textarea`', () => { - expect(vm.$refs.textarea).not.toBeNull(); - }); -}); diff --git a/spec/javascripts/issue_show/components/fields/title_spec.js b/spec/javascripts/issue_show/components/fields/title_spec.js deleted file mode 100644 index 62dff983250..00000000000 --- a/spec/javascripts/issue_show/components/fields/title_spec.js +++ /dev/null @@ -1,48 +0,0 @@ -import Vue from 'vue'; -import eventHub from '~/issue_show/event_hub'; -import Store from '~/issue_show/stores'; -import titleField from '~/issue_show/components/fields/title.vue'; -import { keyboardDownEvent } from '../../helpers'; - -describe('Title field component', () => { - let vm; - let store; - - beforeEach(() => { - const Component = Vue.extend(titleField); - store = new Store({ - titleHtml: '', - descriptionHtml: '', - issuableRef: '', - }); - store.formState.title = 'test'; - - spyOn(eventHub, '$emit'); - - vm = new Component({ - propsData: { - formState: store.formState, - }, - }).$mount(); - }); - - it('renders form control with formState title', () => { - expect(vm.$el.querySelector('.form-control').value).toBe('test'); - }); - - it('triggers update with meta+enter', () => { - vm.$el.querySelector('.form-control').dispatchEvent(keyboardDownEvent(13, true)); - - expect(eventHub.$emit).toHaveBeenCalled(); - }); - - it('triggers update with ctrl+enter', () => { - vm.$el.querySelector('.form-control').dispatchEvent(keyboardDownEvent(13, false, true)); - - expect(eventHub.$emit).toHaveBeenCalled(); - }); - - it('has a ref named `input`', () => { - expect(vm.$refs.input).not.toBeNull(); - }); -}); diff --git a/spec/javascripts/issue_show/index_spec.js b/spec/javascripts/issue_show/index_spec.js deleted file mode 100644 index fa0b426c06c..00000000000 --- a/spec/javascripts/issue_show/index_spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import initIssueableApp from '~/issue_show'; - -describe('Issue show index', () => { - describe('initIssueableApp', () => { - it('should initialize app with no potential XSS attack', () => { - const d = document.createElement('div'); - d.id = 'js-issuable-app-initial-data'; - d.innerHTML = JSON.stringify({ - initialDescriptionHtml: '<img src=x onerror=alert(1)>', - }); - document.body.appendChild(d); - - const alertSpy = spyOn(window, 'alert'); - initIssueableApp(); - - expect(alertSpy).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/javascripts/jobs/components/job_log_controllers_spec.js b/spec/javascripts/jobs/components/job_log_controllers_spec.js deleted file mode 100644 index d527c6708fc..00000000000 --- a/spec/javascripts/jobs/components/job_log_controllers_spec.js +++ /dev/null @@ -1,208 +0,0 @@ -import Vue from 'vue'; -import component from '~/jobs/components/job_log_controllers.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; - -describe('Job log controllers', () => { - const Component = Vue.extend(component); - let vm; - - afterEach(() => { - vm.$destroy(); - }); - - const props = { - rawPath: '/raw', - erasePath: '/erase', - size: 511952, - isScrollTopDisabled: false, - isScrollBottomDisabled: false, - isScrollingDown: true, - isTraceSizeVisible: true, - }; - - describe('Truncate information', () => { - describe('with isTraceSizeVisible', () => { - beforeEach(() => { - vm = mountComponent(Component, props); - }); - - it('renders size information', () => { - expect(vm.$el.querySelector('.js-truncated-info').textContent).toContain('499.95 KiB'); - }); - - it('renders link to raw trace', () => { - expect(vm.$el.querySelector('.js-raw-link').getAttribute('href')).toEqual('/raw'); - }); - }); - }); - - describe('links section', () => { - describe('with raw trace path', () => { - it('renders raw trace link', () => { - vm = mountComponent(Component, props); - - expect(vm.$el.querySelector('.js-raw-link-controller').getAttribute('href')).toEqual( - '/raw', - ); - }); - }); - - describe('without raw trace path', () => { - it('does not render raw trace link', () => { - vm = mountComponent(Component, { - erasePath: '/erase', - size: 511952, - isScrollTopDisabled: true, - isScrollBottomDisabled: true, - isScrollingDown: false, - isTraceSizeVisible: true, - }); - - expect(vm.$el.querySelector('.js-raw-link-controller')).toBeNull(); - }); - }); - - describe('when is erasable', () => { - beforeEach(() => { - vm = mountComponent(Component, props); - }); - - it('renders erase job link', () => { - expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull(); - }); - }); - - describe('when it is not erasable', () => { - it('does not render erase button', () => { - vm = mountComponent(Component, { - rawPath: '/raw', - size: 511952, - isScrollTopDisabled: true, - isScrollBottomDisabled: true, - isScrollingDown: false, - isTraceSizeVisible: true, - }); - - expect(vm.$el.querySelector('.js-erase-link')).toBeNull(); - }); - }); - }); - - describe('scroll buttons', () => { - describe('scroll top button', () => { - describe('when user can scroll top', () => { - beforeEach(() => { - vm = mountComponent(Component, props); - }); - - it('renders enabled scroll top button', () => { - expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toBeNull(); - }); - - it('emits scrollJobLogTop event on click', () => { - spyOn(vm, '$emit'); - vm.$el.querySelector('.js-scroll-top').click(); - - expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogTop'); - }); - }); - - describe('when user can not scroll top', () => { - beforeEach(() => { - vm = mountComponent(Component, { - rawPath: '/raw', - erasePath: '/erase', - size: 511952, - isScrollTopDisabled: true, - isScrollBottomDisabled: false, - isScrollingDown: false, - isTraceSizeVisible: true, - }); - }); - - it('renders disabled scroll top button', () => { - expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toEqual( - 'disabled', - ); - }); - - it('does not emit scrollJobLogTop event on click', () => { - spyOn(vm, '$emit'); - vm.$el.querySelector('.js-scroll-top').click(); - - expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogTop'); - }); - }); - }); - - describe('scroll bottom button', () => { - describe('when user can scroll bottom', () => { - beforeEach(() => { - vm = mountComponent(Component, props); - }); - - it('renders enabled scroll bottom button', () => { - expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toBeNull(); - }); - - it('emits scrollJobLogBottom event on click', () => { - spyOn(vm, '$emit'); - vm.$el.querySelector('.js-scroll-bottom').click(); - - expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogBottom'); - }); - }); - - describe('when user can not scroll bottom', () => { - beforeEach(() => { - vm = mountComponent(Component, { - rawPath: '/raw', - erasePath: '/erase', - size: 511952, - isScrollTopDisabled: false, - isScrollBottomDisabled: true, - isScrollingDown: false, - isTraceSizeVisible: true, - }); - }); - - it('renders disabled scroll bottom button', () => { - expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toEqual( - 'disabled', - ); - }); - - it('does not emit scrollJobLogBottom event on click', () => { - spyOn(vm, '$emit'); - vm.$el.querySelector('.js-scroll-bottom').click(); - - expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogBottom'); - }); - }); - - describe('while isScrollingDown is true', () => { - it('renders animate class for the scroll down button', () => { - vm = mountComponent(Component, props); - - expect(vm.$el.querySelector('.js-scroll-bottom').className).toContain('animate'); - }); - }); - - describe('while isScrollingDown is false', () => { - it('does not render animate class for the scroll down button', () => { - vm = mountComponent(Component, { - rawPath: '/raw', - erasePath: '/erase', - size: 511952, - isScrollTopDisabled: true, - isScrollBottomDisabled: false, - isScrollingDown: false, - isTraceSizeVisible: true, - }); - - expect(vm.$el.querySelector('.js-scroll-bottom').className).not.toContain('animate'); - }); - }); - }); - }); -}); diff --git a/spec/javascripts/namespace_select_spec.js b/spec/javascripts/namespace_select_spec.js deleted file mode 100644 index 07b82ce721e..00000000000 --- a/spec/javascripts/namespace_select_spec.js +++ /dev/null @@ -1,66 +0,0 @@ -import $ from 'jquery'; -import NamespaceSelect from '~/namespace_select'; - -describe('NamespaceSelect', () => { - beforeEach(() => { - spyOn($.fn, 'glDropdown'); - }); - - it('initializes glDropdown', () => { - const dropdown = document.createElement('div'); - - // eslint-disable-next-line no-new - new NamespaceSelect({ dropdown }); - - expect($.fn.glDropdown).toHaveBeenCalled(); - }); - - describe('as input', () => { - let glDropdownOptions; - - beforeEach(() => { - const dropdown = document.createElement('div'); - // eslint-disable-next-line no-new - new NamespaceSelect({ dropdown }); - [glDropdownOptions] = $.fn.glDropdown.calls.argsFor(0); - }); - - it('prevents click events', () => { - const dummyEvent = new Event('dummy'); - spyOn(dummyEvent, 'preventDefault'); - - glDropdownOptions.clicked({ e: dummyEvent }); - - expect(dummyEvent.preventDefault).toHaveBeenCalled(); - }); - }); - - describe('as filter', () => { - let glDropdownOptions; - - beforeEach(() => { - const dropdown = document.createElement('div'); - dropdown.dataset.isFilter = 'true'; - // eslint-disable-next-line no-new - new NamespaceSelect({ dropdown }); - [glDropdownOptions] = $.fn.glDropdown.calls.argsFor(0); - }); - - it('does not prevent click events', () => { - const dummyEvent = new Event('dummy'); - spyOn(dummyEvent, 'preventDefault'); - - glDropdownOptions.clicked({ e: dummyEvent }); - - expect(dummyEvent.preventDefault).not.toHaveBeenCalled(); - }); - - it('sets URL of dropdown items', () => { - const dummyNamespace = { id: 'eal' }; - - const itemUrl = glDropdownOptions.url(dummyNamespace); - - expect(itemUrl).toContain(`namespace_id=${dummyNamespace.id}`); - }); - }); -}); diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js deleted file mode 100644 index 4e3140ce4f1..00000000000 --- a/spec/javascripts/new_branch_spec.js +++ /dev/null @@ -1,199 +0,0 @@ -import $ from 'jquery'; -import NewBranchForm from '~/new_branch_form'; - -describe('Branch', function() { - describe('create a new branch', function() { - preloadFixtures('branches/new_branch.html'); - - function fillNameWith(value) { - $('.js-branch-name') - .val(value) - .trigger('blur'); - } - - function expectToHaveError(error) { - expect($('.js-branch-name-error span').text()).toEqual(error); - } - - beforeEach(function() { - loadFixtures('branches/new_branch.html'); - $('form').on('submit', function(e) { - return e.preventDefault(); - }); - this.form = new NewBranchForm($('.js-create-branch-form'), []); - }); - - it("can't start with a dot", function() { - fillNameWith('.foo'); - expectToHaveError("can't start with '.'"); - }); - - it("can't start with a slash", function() { - fillNameWith('/foo'); - expectToHaveError("can't start with '/'"); - }); - - it("can't have two consecutive dots", function() { - fillNameWith('foo..bar'); - expectToHaveError("can't contain '..'"); - }); - - it("can't have spaces anywhere", function() { - fillNameWith(' foo'); - expectToHaveError("can't contain spaces"); - fillNameWith('foo bar'); - expectToHaveError("can't contain spaces"); - fillNameWith('foo '); - expectToHaveError("can't contain spaces"); - }); - - it("can't have ~ anywhere", function() { - fillNameWith('~foo'); - expectToHaveError("can't contain '~'"); - fillNameWith('foo~bar'); - expectToHaveError("can't contain '~'"); - fillNameWith('foo~'); - expectToHaveError("can't contain '~'"); - }); - - it("can't have tilde anwhere", function() { - fillNameWith('~foo'); - expectToHaveError("can't contain '~'"); - fillNameWith('foo~bar'); - expectToHaveError("can't contain '~'"); - fillNameWith('foo~'); - expectToHaveError("can't contain '~'"); - }); - - it("can't have caret anywhere", function() { - fillNameWith('^foo'); - expectToHaveError("can't contain '^'"); - fillNameWith('foo^bar'); - expectToHaveError("can't contain '^'"); - fillNameWith('foo^'); - expectToHaveError("can't contain '^'"); - }); - - it("can't have : anywhere", function() { - fillNameWith(':foo'); - expectToHaveError("can't contain ':'"); - fillNameWith('foo:bar'); - expectToHaveError("can't contain ':'"); - fillNameWith(':foo'); - expectToHaveError("can't contain ':'"); - }); - - it("can't have question mark anywhere", function() { - fillNameWith('?foo'); - expectToHaveError("can't contain '?'"); - fillNameWith('foo?bar'); - expectToHaveError("can't contain '?'"); - fillNameWith('foo?'); - expectToHaveError("can't contain '?'"); - }); - - it("can't have asterisk anywhere", function() { - fillNameWith('*foo'); - expectToHaveError("can't contain '*'"); - fillNameWith('foo*bar'); - expectToHaveError("can't contain '*'"); - fillNameWith('foo*'); - expectToHaveError("can't contain '*'"); - }); - - it("can't have open bracket anywhere", function() { - fillNameWith('[foo'); - expectToHaveError("can't contain '['"); - fillNameWith('foo[bar'); - expectToHaveError("can't contain '['"); - fillNameWith('foo['); - expectToHaveError("can't contain '['"); - }); - - it("can't have a backslash anywhere", function() { - fillNameWith('\\foo'); - expectToHaveError("can't contain '\\'"); - fillNameWith('foo\\bar'); - expectToHaveError("can't contain '\\'"); - fillNameWith('foo\\'); - expectToHaveError("can't contain '\\'"); - }); - - it("can't contain a sequence @{ anywhere", function() { - fillNameWith('@{foo'); - expectToHaveError("can't contain '@{'"); - fillNameWith('foo@{bar'); - expectToHaveError("can't contain '@{'"); - fillNameWith('foo@{'); - expectToHaveError("can't contain '@{'"); - }); - - it("can't have consecutive slashes", function() { - fillNameWith('foo//bar'); - expectToHaveError("can't contain consecutive slashes"); - }); - - it("can't end with a slash", function() { - fillNameWith('foo/'); - expectToHaveError("can't end in '/'"); - }); - - it("can't end with a dot", function() { - fillNameWith('foo.'); - expectToHaveError("can't end in '.'"); - }); - - it("can't end with .lock", function() { - fillNameWith('foo.lock'); - expectToHaveError("can't end in '.lock'"); - }); - - it("can't be the single character @", function() { - fillNameWith('@'); - expectToHaveError("can't be '@'"); - }); - - it('concatenates all error messages', function() { - fillNameWith('/foo bar?~.'); - expectToHaveError("can't start with '/', can't contain spaces, '?', '~', can't end in '.'"); - }); - - it("doesn't duplicate error messages", function() { - fillNameWith('?foo?bar?zoo?'); - expectToHaveError("can't contain '?'"); - }); - - it('removes the error message when is a valid name', function() { - fillNameWith('foo?bar'); - - expect($('.js-branch-name-error span').length).toEqual(1); - fillNameWith('foobar'); - - expect($('.js-branch-name-error span').length).toEqual(0); - }); - - it('can have dashes anywhere', function() { - fillNameWith('-foo-bar-zoo-'); - - expect($('.js-branch-name-error span').length).toEqual(0); - }); - - it('can have underscores anywhere', function() { - fillNameWith('_foo_bar_zoo_'); - - expect($('.js-branch-name-error span').length).toEqual(0); - }); - - it('can have numbers anywhere', function() { - fillNameWith('1foo2bar3zoo4'); - - expect($('.js-branch-name-error span').length).toEqual(0); - }); - - it('can be only letters', function() { - fillNameWith('foo'); - - expect($('.js-branch-name-error span').length).toEqual(0); - }); - }); -}); diff --git a/spec/javascripts/notes/components/discussion_filter_note_spec.js b/spec/javascripts/notes/components/discussion_filter_note_spec.js deleted file mode 100644 index 52d2e7ce947..00000000000 --- a/spec/javascripts/notes/components/discussion_filter_note_spec.js +++ /dev/null @@ -1,93 +0,0 @@ -import Vue from 'vue'; -import DiscussionFilterNote from '~/notes/components/discussion_filter_note.vue'; -import eventHub from '~/notes/event_hub'; - -import mountComponent from '../../helpers/vue_mount_component_helper'; - -describe('DiscussionFilterNote component', () => { - let vm; - - const createComponent = () => { - const Component = Vue.extend(DiscussionFilterNote); - - return mountComponent(Component); - }; - - beforeEach(() => { - vm = createComponent(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('computed', () => { - describe('timelineContent', () => { - it('returns string containing instruction for switching feed type', () => { - expect(vm.timelineContent).toBe( - "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", - ); - }); - }); - }); - - describe('methods', () => { - describe('selectFilter', () => { - it('emits `dropdownSelect` event on `eventHub` with provided param', () => { - spyOn(eventHub, '$emit'); - - vm.selectFilter(1); - - expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1); - }); - }); - }); - - describe('template', () => { - it('renders component container element', () => { - expect(vm.$el.classList.contains('discussion-filter-note')).toBe(true); - }); - - it('renders comment icon element', () => { - expect(vm.$el.querySelector('.timeline-icon svg use').getAttribute('xlink:href')).toContain( - 'comment', - ); - }); - - it('renders filter information note', () => { - expect(vm.$el.querySelector('.timeline-content').innerText.trim()).toContain( - "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.", - ); - }); - - it('renders filter buttons', () => { - const buttonsContainerEl = vm.$el.querySelector('.discussion-filter-actions'); - - expect(buttonsContainerEl.querySelector('button:first-child').innerText.trim()).toContain( - 'Show all activity', - ); - - expect(buttonsContainerEl.querySelector('button:last-child').innerText.trim()).toContain( - 'Show comments only', - ); - }); - - it('clicking `Show all activity` button calls `selectFilter("all")` method', () => { - const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:first-child'); - spyOn(vm, 'selectFilter'); - - showAllBtn.dispatchEvent(new Event('click')); - - expect(vm.selectFilter).toHaveBeenCalledWith(0); - }); - - it('clicking `Show comments only` button calls `selectFilter("comments")` method', () => { - const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:last-child'); - spyOn(vm, 'selectFilter'); - - showAllBtn.dispatchEvent(new Event('click')); - - expect(vm.selectFilter).toHaveBeenCalledWith(1); - }); - }); -}); diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js deleted file mode 100644 index 6d1a7ef370f..00000000000 --- a/spec/javascripts/notes/components/note_header_spec.js +++ /dev/null @@ -1,125 +0,0 @@ -import Vue from 'vue'; -import noteHeader from '~/notes/components/note_header.vue'; -import createStore from '~/notes/stores'; - -describe('note_header component', () => { - let store; - let vm; - let Component; - - beforeEach(() => { - Component = Vue.extend(noteHeader); - store = createStore(); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('individual note', () => { - beforeEach(() => { - vm = new Component({ - store, - propsData: { - actionText: 'commented', - actionTextHtml: '', - author: { - avatar_url: null, - id: 1, - name: 'Root', - path: '/root', - state: 'active', - username: 'root', - }, - createdAt: '2017-08-02T10:51:58.559Z', - includeToggle: false, - noteId: '1394', - expanded: true, - }, - }).$mount(); - }); - - it('should render user information', () => { - expect(vm.$el.querySelector('.note-header-author-name').textContent.trim()).toEqual('Root'); - expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual('/root'); - expect(vm.$el.querySelector('.note-header-info a').dataset.userId).toEqual('1'); - expect(vm.$el.querySelector('.note-header-info a').dataset.username).toEqual('root'); - expect(vm.$el.querySelector('.note-header-info a').classList).toContain('js-user-link'); - }); - - it('should render timestamp link', () => { - expect(vm.$el.querySelector('a[href="#note_1394"]')).toBeDefined(); - }); - - it('should not render user information when prop `author` is empty object', done => { - vm.author = {}; - Vue.nextTick() - .then(() => { - expect(vm.$el.querySelector('.note-header-author-name')).toBeNull(); - }) - .then(done) - .catch(done.fail); - }); - }); - - describe('discussion', () => { - beforeEach(() => { - vm = new Component({ - store, - propsData: { - actionText: 'started a discussion', - actionTextHtml: '', - author: { - avatar_url: null, - id: 1, - name: 'Root', - path: '/root', - state: 'active', - username: 'root', - }, - createdAt: '2017-08-02T10:51:58.559Z', - includeToggle: true, - noteId: '1395', - expanded: true, - }, - }).$mount(); - }); - - it('should render toggle button', () => { - expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined(); - }); - - it('emits toggle event on click', done => { - spyOn(vm, '$emit'); - - vm.$el.querySelector('.js-vue-toggle-button').click(); - - Vue.nextTick(() => { - expect(vm.$emit).toHaveBeenCalledWith('toggleHandler'); - done(); - }); - }); - - it('renders up arrow when open', done => { - vm.expanded = true; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( - 'fa-chevron-up', - ); - done(); - }); - }); - - it('renders down arrow when closed', done => { - vm.expanded = false; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.js-vue-toggle-button i').classList).toContain( - 'fa-chevron-down', - ); - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js deleted file mode 100644 index d69f469c7c7..00000000000 --- a/spec/javascripts/notes/stores/getters_spec.js +++ /dev/null @@ -1,388 +0,0 @@ -import * as getters from '~/notes/stores/getters'; -import { - notesDataMock, - userDataMock, - noteableDataMock, - individualNote, - collapseNotesMock, - discussion1, - discussion2, - discussion3, - resolvedDiscussion1, - unresolvableDiscussion, -} from '../mock_data'; - -const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json'; - -// Helper function to ensure that we're using the same schema across tests. -const createDiscussionNeighborParams = (discussionId, diffOrder, step) => ({ - discussionId, - diffOrder, - step, -}); - -describe('Getters Notes Store', () => { - let state; - - preloadFixtures(discussionWithTwoUnresolvedNotes); - - beforeEach(() => { - state = { - discussions: [individualNote], - targetNoteHash: 'hash', - lastFetchedAt: 'timestamp', - isNotesFetched: false, - notesData: notesDataMock, - userData: userDataMock, - noteableData: noteableDataMock, - }; - }); - - describe('showJumpToNextDiscussion', () => { - it('should return true if there are 2 or more unresolved discussions', () => { - const localGetters = { - unresolvedDiscussionsIdsByDate: ['123', '456'], - allResolvableDiscussions: [], - }; - - expect(getters.showJumpToNextDiscussion(state, localGetters)()).toBe(true); - }); - - it('should return false if there are 1 or less unresolved discussions', () => { - const localGetters = { - unresolvedDiscussionsIdsByDate: ['123'], - allResolvableDiscussions: [], - }; - - expect(getters.showJumpToNextDiscussion(state, localGetters)()).toBe(false); - }); - }); - - describe('discussions', () => { - it('should return all discussions in the store', () => { - expect(getters.discussions(state)).toEqual([individualNote]); - }); - }); - - describe('resolvedDiscussionsById', () => { - it('ignores unresolved system notes', () => { - const [discussion] = getJSONFixture(discussionWithTwoUnresolvedNotes); - discussion.notes[0].resolved = true; - discussion.notes[1].resolved = false; - state.discussions.push(discussion); - - expect(getters.resolvedDiscussionsById(state)).toEqual({ - [discussion.id]: discussion, - }); - }); - }); - - describe('Collapsed notes', () => { - const stateCollapsedNotes = { - discussions: collapseNotesMock, - targetNoteHash: 'hash', - lastFetchedAt: 'timestamp', - - notesData: notesDataMock, - userData: userDataMock, - noteableData: noteableDataMock, - }; - - it('should return a single system note when a description was updated multiple times', () => { - expect(getters.discussions(stateCollapsedNotes).length).toEqual(1); - }); - }); - - describe('targetNoteHash', () => { - it('should return `targetNoteHash`', () => { - expect(getters.targetNoteHash(state)).toEqual('hash'); - }); - }); - - describe('getNotesData', () => { - it('should return all data in `notesData`', () => { - expect(getters.getNotesData(state)).toEqual(notesDataMock); - }); - }); - - describe('getNoteableData', () => { - it('should return all data in `noteableData`', () => { - expect(getters.getNoteableData(state)).toEqual(noteableDataMock); - }); - }); - - describe('getUserData', () => { - it('should return all data in `userData`', () => { - expect(getters.getUserData(state)).toEqual(userDataMock); - }); - }); - - describe('notesById', () => { - it('should return the note for the given id', () => { - expect(getters.notesById(state)).toEqual({ 1390: individualNote.notes[0] }); - }); - }); - - describe('getCurrentUserLastNote', () => { - it('should return the last note of the current user', () => { - expect(getters.getCurrentUserLastNote(state)).toEqual(individualNote.notes[0]); - }); - }); - - describe('openState', () => { - it('should return the issue state', () => { - expect(getters.openState(state)).toEqual(noteableDataMock.state); - }); - }); - - describe('isNotesFetched', () => { - it('should return the state for the fetching notes', () => { - expect(getters.isNotesFetched(state)).toBeFalsy(); - }); - }); - - describe('allResolvableDiscussions', () => { - it('should return only resolvable discussions in same order', () => { - state.discussions = [ - discussion3, - unresolvableDiscussion, - discussion1, - unresolvableDiscussion, - discussion2, - ]; - - expect(getters.allResolvableDiscussions(state)).toEqual([ - discussion3, - discussion1, - discussion2, - ]); - }); - - it('should return empty array if there are no resolvable discussions', () => { - state.discussions = [unresolvableDiscussion, unresolvableDiscussion]; - - expect(getters.allResolvableDiscussions(state)).toEqual([]); - }); - }); - - describe('unresolvedDiscussionsIdsByDiff', () => { - it('should return all discussions IDs in diff order', () => { - const localGetters = { - allResolvableDiscussions: [discussion3, discussion1, discussion2], - }; - - expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters)).toEqual([ - 'abc1', - 'abc2', - 'abc3', - ]); - }); - - it('should return empty array if all discussions have been resolved', () => { - const localGetters = { - allResolvableDiscussions: [resolvedDiscussion1], - }; - - expect(getters.unresolvedDiscussionsIdsByDiff(state, localGetters)).toEqual([]); - }); - }); - - describe('unresolvedDiscussionsIdsByDate', () => { - it('should return all discussions in date ascending order', () => { - const localGetters = { - allResolvableDiscussions: [discussion3, discussion1, discussion2], - }; - - expect(getters.unresolvedDiscussionsIdsByDate(state, localGetters)).toEqual([ - 'abc2', - 'abc1', - 'abc3', - ]); - }); - - it('should return empty array if all discussions have been resolved', () => { - const localGetters = { - allResolvableDiscussions: [resolvedDiscussion1], - }; - - expect(getters.unresolvedDiscussionsIdsByDate(state, localGetters)).toEqual([]); - }); - }); - - describe('unresolvedDiscussionsIdsOrdered', () => { - const localGetters = { - unresolvedDiscussionsIdsByDate: ['123', '456'], - unresolvedDiscussionsIdsByDiff: ['abc', 'def'], - }; - - it('should return IDs ordered by diff when diffOrder param is true', () => { - expect(getters.unresolvedDiscussionsIdsOrdered(state, localGetters)(true)).toEqual([ - 'abc', - 'def', - ]); - }); - - it('should return IDs ordered by date when diffOrder param is not true', () => { - expect(getters.unresolvedDiscussionsIdsOrdered(state, localGetters)(false)).toEqual([ - '123', - '456', - ]); - - expect(getters.unresolvedDiscussionsIdsOrdered(state, localGetters)(undefined)).toEqual([ - '123', - '456', - ]); - }); - }); - - describe('isLastUnresolvedDiscussion', () => { - const localGetters = { - unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'], - }; - - it('should return true if the discussion id provided is the last', () => { - expect(getters.isLastUnresolvedDiscussion(state, localGetters)('789')).toBe(true); - }); - - it('should return false if the discussion id provided is not the last', () => { - expect(getters.isLastUnresolvedDiscussion(state, localGetters)('123')).toBe(false); - expect(getters.isLastUnresolvedDiscussion(state, localGetters)('456')).toBe(false); - }); - }); - - describe('findUnresolvedDiscussionIdNeighbor', () => { - let localGetters; - beforeEach(() => { - localGetters = { - unresolvedDiscussionsIdsOrdered: () => ['123', '456', '789'], - }; - }); - - [ - { step: 1, id: '123', expected: '456' }, - { step: 1, id: '456', expected: '789' }, - { step: 1, id: '789', expected: '123' }, - { step: -1, id: '123', expected: '789' }, - { step: -1, id: '456', expected: '123' }, - { step: -1, id: '789', expected: '456' }, - ].forEach(({ step, id, expected }) => { - it(`with step ${step} and id ${id}, returns next value`, () => { - const params = createDiscussionNeighborParams(id, true, step); - - expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe( - expected, - ); - }); - }); - - describe('with 1 unresolved discussion', () => { - beforeEach(() => { - localGetters = { - unresolvedDiscussionsIdsOrdered: () => ['123'], - }; - }); - - [{ step: 1, id: '123', expected: '123' }, { step: -1, id: '123', expected: '123' }].forEach( - ({ step, id, expected }) => { - it(`with step ${step} and match, returns only value`, () => { - const params = createDiscussionNeighborParams(id, true, step); - - expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe( - expected, - ); - }); - }, - ); - - it('with no match, returns only value', () => { - const params = createDiscussionNeighborParams('bogus', true, 1); - - expect(getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params)).toBe('123'); - }); - }); - - describe('with 0 unresolved discussions', () => { - beforeEach(() => { - localGetters = { - unresolvedDiscussionsIdsOrdered: () => [], - }; - }); - - [{ step: 1 }, { step: -1 }].forEach(({ step }) => { - it(`with step ${step}, returns undefined`, () => { - const params = createDiscussionNeighborParams('bogus', true, step); - - expect( - getters.findUnresolvedDiscussionIdNeighbor(state, localGetters)(params), - ).toBeUndefined(); - }); - }); - }); - }); - - describe('findUnresolvedDiscussionIdNeighbor aliases', () => { - let neighbor; - let findUnresolvedDiscussionIdNeighbor; - let localGetters; - - beforeEach(() => { - neighbor = {}; - findUnresolvedDiscussionIdNeighbor = jasmine.createSpy().and.returnValue(neighbor); - localGetters = { findUnresolvedDiscussionIdNeighbor }; - }); - - describe('nextUnresolvedDiscussionId', () => { - it('should return result of find neighbor', () => { - const expectedParams = createDiscussionNeighborParams('123', true, 1); - const result = getters.nextUnresolvedDiscussionId(state, localGetters)('123', true); - - expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams); - expect(result).toBe(neighbor); - }); - }); - - describe('previosuUnresolvedDiscussionId', () => { - it('should return result of find neighbor', () => { - const expectedParams = createDiscussionNeighborParams('123', true, -1); - const result = getters.previousUnresolvedDiscussionId(state, localGetters)('123', true); - - expect(findUnresolvedDiscussionIdNeighbor).toHaveBeenCalledWith(expectedParams); - expect(result).toBe(neighbor); - }); - }); - }); - - describe('firstUnresolvedDiscussionId', () => { - const localGetters = { - unresolvedDiscussionsIdsByDate: ['123', '456'], - unresolvedDiscussionsIdsByDiff: ['abc', 'def'], - }; - - it('should return the first discussion id by diff when diffOrder param is true', () => { - expect(getters.firstUnresolvedDiscussionId(state, localGetters)(true)).toBe('abc'); - }); - - it('should return the first discussion id by date when diffOrder param is not true', () => { - expect(getters.firstUnresolvedDiscussionId(state, localGetters)(false)).toBe('123'); - expect(getters.firstUnresolvedDiscussionId(state, localGetters)(undefined)).toBe('123'); - }); - - it('should be falsy if all discussions are resolved', () => { - const localGettersFalsy = { - unresolvedDiscussionsIdsByDiff: [], - unresolvedDiscussionsIdsByDate: [], - }; - - expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(true)).toBeFalsy(); - expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(false)).toBeFalsy(); - }); - }); - - describe('getDiscussion', () => { - it('returns discussion by ID', () => { - state.discussions.push({ id: '1' }); - - expect(getters.getDiscussion(state)('1')).toEqual({ id: '1' }); - }); - }); -}); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js deleted file mode 100644 index ade4725dd68..00000000000 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ /dev/null @@ -1,584 +0,0 @@ -import Vue from 'vue'; -import mutations from '~/notes/stores/mutations'; -import { DISCUSSION_NOTE } from '~/notes/constants'; -import { - note, - discussionMock, - notesDataMock, - userDataMock, - noteableDataMock, - individualNote, -} from '../mock_data'; - -const RESOLVED_NOTE = { resolvable: true, resolved: true }; -const UNRESOLVED_NOTE = { resolvable: true, resolved: false }; -const SYSTEM_NOTE = { resolvable: false, resolved: false }; -const WEIRD_NOTE = { resolvable: false, resolved: true }; - -describe('Notes Store mutations', () => { - describe('ADD_NEW_NOTE', () => { - let state; - let noteData; - - beforeEach(() => { - state = { discussions: [] }; - noteData = { - expanded: true, - id: note.discussion_id, - individual_note: true, - notes: [note], - reply_id: note.discussion_id, - }; - mutations.ADD_NEW_NOTE(state, note); - }); - - it('should add a new note to an array of notes', () => { - expect(state).toEqual({ - discussions: [noteData], - }); - - expect(state.discussions.length).toBe(1); - }); - - it('should not add the same note to the notes array', () => { - mutations.ADD_NEW_NOTE(state, note); - - expect(state.discussions.length).toBe(1); - }); - }); - - describe('ADD_NEW_REPLY_TO_DISCUSSION', () => { - const newReply = Object.assign({}, note, { discussion_id: discussionMock.id }); - - let state; - - beforeEach(() => { - state = { discussions: [{ ...discussionMock }] }; - }); - - it('should add a reply to a specific discussion', () => { - mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); - - expect(state.discussions[0].notes.length).toEqual(4); - }); - - it('should not add the note if it already exists in the discussion', () => { - mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); - mutations.ADD_NEW_REPLY_TO_DISCUSSION(state, newReply); - - expect(state.discussions[0].notes.length).toEqual(4); - }); - }); - - describe('DELETE_NOTE', () => { - it('should delete a note ', () => { - const state = { discussions: [discussionMock] }; - const toDelete = discussionMock.notes[0]; - const lengthBefore = discussionMock.notes.length; - - mutations.DELETE_NOTE(state, toDelete); - - expect(state.discussions[0].notes.length).toEqual(lengthBefore - 1); - }); - }); - - describe('EXPAND_DISCUSSION', () => { - it('should expand a collapsed discussion', () => { - const discussion = Object.assign({}, discussionMock, { expanded: false }); - - const state = { - discussions: [discussion], - }; - - mutations.EXPAND_DISCUSSION(state, { discussionId: discussion.id }); - - expect(state.discussions[0].expanded).toEqual(true); - }); - }); - - describe('COLLAPSE_DISCUSSION', () => { - it('should collapse an expanded discussion', () => { - const discussion = Object.assign({}, discussionMock, { expanded: true }); - - const state = { - discussions: [discussion], - }; - - mutations.COLLAPSE_DISCUSSION(state, { discussionId: discussion.id }); - - expect(state.discussions[0].expanded).toEqual(false); - }); - }); - - describe('REMOVE_PLACEHOLDER_NOTES', () => { - it('should remove all placeholder notes in indivudal notes and discussion', () => { - const placeholderNote = Object.assign({}, individualNote, { isPlaceholderNote: true }); - const state = { discussions: [placeholderNote] }; - mutations.REMOVE_PLACEHOLDER_NOTES(state); - - expect(state.discussions).toEqual([]); - }); - }); - - describe('SET_NOTES_DATA', () => { - it('should set an object with notesData', () => { - const state = { - notesData: {}, - }; - - mutations.SET_NOTES_DATA(state, notesDataMock); - - expect(state.notesData).toEqual(notesDataMock); - }); - }); - - describe('SET_NOTEABLE_DATA', () => { - it('should set the issue data', () => { - const state = { - noteableData: {}, - }; - - mutations.SET_NOTEABLE_DATA(state, noteableDataMock); - - expect(state.noteableData).toEqual(noteableDataMock); - }); - }); - - describe('SET_USER_DATA', () => { - it('should set the user data', () => { - const state = { - userData: {}, - }; - - mutations.SET_USER_DATA(state, userDataMock); - - expect(state.userData).toEqual(userDataMock); - }); - }); - - describe('SET_INITIAL_DISCUSSIONS', () => { - it('should set the initial notes received', () => { - const state = { - discussions: [], - }; - const legacyNote = { - id: 2, - individual_note: true, - notes: [ - { - note: '1', - }, - { - note: '2', - }, - ], - }; - - mutations.SET_INITIAL_DISCUSSIONS(state, [note, legacyNote]); - - expect(state.discussions[0].id).toEqual(note.id); - expect(state.discussions[1].notes[0].note).toBe(legacyNote.notes[0].note); - expect(state.discussions[2].notes[0].note).toBe(legacyNote.notes[1].note); - expect(state.discussions.length).toEqual(3); - }); - - it('adds truncated_diff_lines if discussion is a diffFile', () => { - const state = { - discussions: [], - }; - - mutations.SET_INITIAL_DISCUSSIONS(state, [ - { - ...note, - diff_file: { - file_hash: 'a', - }, - truncated_diff_lines: [{ text: '+a', rich_text: '+a' }], - }, - ]); - - expect(state.discussions[0].truncated_diff_lines).toEqual([{ rich_text: 'a' }]); - }); - - it('adds empty truncated_diff_lines when not in discussion', () => { - const state = { - discussions: [], - }; - - mutations.SET_INITIAL_DISCUSSIONS(state, [ - { - ...note, - diff_file: { - file_hash: 'a', - }, - }, - ]); - - expect(state.discussions[0].truncated_diff_lines).toEqual([]); - }); - }); - - describe('SET_LAST_FETCHED_AT', () => { - it('should set timestamp', () => { - const state = { - lastFetchedAt: [], - }; - - mutations.SET_LAST_FETCHED_AT(state, 'timestamp'); - - expect(state.lastFetchedAt).toEqual('timestamp'); - }); - }); - - describe('SET_TARGET_NOTE_HASH', () => { - it('should set the note hash', () => { - const state = { - targetNoteHash: [], - }; - - mutations.SET_TARGET_NOTE_HASH(state, 'hash'); - - expect(state.targetNoteHash).toEqual('hash'); - }); - }); - - describe('SHOW_PLACEHOLDER_NOTE', () => { - it('should set a placeholder note', () => { - const state = { - discussions: [], - }; - mutations.SHOW_PLACEHOLDER_NOTE(state, note); - - expect(state.discussions[0].isPlaceholderNote).toEqual(true); - }); - }); - - describe('TOGGLE_AWARD', () => { - it('should add award if user has not reacted yet', () => { - const state = { - discussions: [note], - userData: userDataMock, - }; - - const data = { - note, - awardName: 'cartwheel', - }; - - mutations.TOGGLE_AWARD(state, data); - const lastIndex = state.discussions[0].award_emoji.length - 1; - - expect(state.discussions[0].award_emoji[lastIndex]).toEqual({ - name: 'cartwheel', - user: { id: userDataMock.id, name: userDataMock.name, username: userDataMock.username }, - }); - }); - - it('should remove award if user already reacted', () => { - const state = { - discussions: [note], - userData: { - id: 1, - name: 'Administrator', - username: 'root', - }, - }; - - const data = { - note, - awardName: 'bath_tone3', - }; - mutations.TOGGLE_AWARD(state, data); - - expect(state.discussions[0].award_emoji.length).toEqual(2); - }); - }); - - describe('TOGGLE_DISCUSSION', () => { - it('should open a closed discussion', () => { - const discussion = Object.assign({}, discussionMock, { expanded: false }); - - const state = { - discussions: [discussion], - }; - - mutations.TOGGLE_DISCUSSION(state, { discussionId: discussion.id }); - - expect(state.discussions[0].expanded).toEqual(true); - }); - - it('should close a opened discussion', () => { - const state = { - discussions: [discussionMock], - }; - - mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id }); - - expect(state.discussions[0].expanded).toEqual(false); - }); - - it('forces a discussions expanded state', () => { - const state = { - discussions: [{ ...discussionMock, expanded: false }], - }; - - mutations.TOGGLE_DISCUSSION(state, { discussionId: discussionMock.id, forceExpanded: true }); - - expect(state.discussions[0].expanded).toEqual(true); - }); - }); - - describe('UPDATE_NOTE', () => { - it('should update a note', () => { - const state = { - discussions: [individualNote], - }; - - const updated = Object.assign({}, individualNote.notes[0], { note: 'Foo' }); - - mutations.UPDATE_NOTE(state, updated); - - expect(state.discussions[0].notes[0].note).toEqual('Foo'); - }); - - it('transforms an individual note to discussion', () => { - const state = { - discussions: [individualNote], - }; - - const transformedNote = { ...individualNote.notes[0], type: DISCUSSION_NOTE }; - - mutations.UPDATE_NOTE(state, transformedNote); - - expect(state.discussions[0].individual_note).toEqual(false); - }); - }); - - describe('CLOSE_ISSUE', () => { - it('should set issue as closed', () => { - const state = { - discussions: [], - targetNoteHash: null, - lastFetchedAt: null, - isToggleStateButtonLoading: false, - notesData: {}, - userData: {}, - noteableData: {}, - }; - - mutations.CLOSE_ISSUE(state); - - expect(state.noteableData.state).toEqual('closed'); - }); - }); - - describe('REOPEN_ISSUE', () => { - it('should set issue as closed', () => { - const state = { - discussions: [], - targetNoteHash: null, - lastFetchedAt: null, - isToggleStateButtonLoading: false, - notesData: {}, - userData: {}, - noteableData: {}, - }; - - mutations.REOPEN_ISSUE(state); - - expect(state.noteableData.state).toEqual('reopened'); - }); - }); - - describe('TOGGLE_STATE_BUTTON_LOADING', () => { - it('should set isToggleStateButtonLoading as true', () => { - const state = { - discussions: [], - targetNoteHash: null, - lastFetchedAt: null, - isToggleStateButtonLoading: false, - notesData: {}, - userData: {}, - noteableData: {}, - }; - - mutations.TOGGLE_STATE_BUTTON_LOADING(state, true); - - expect(state.isToggleStateButtonLoading).toEqual(true); - }); - - it('should set isToggleStateButtonLoading as false', () => { - const state = { - discussions: [], - targetNoteHash: null, - lastFetchedAt: null, - isToggleStateButtonLoading: true, - notesData: {}, - userData: {}, - noteableData: {}, - }; - - mutations.TOGGLE_STATE_BUTTON_LOADING(state, false); - - expect(state.isToggleStateButtonLoading).toEqual(false); - }); - }); - - describe('SET_NOTES_FETCHED_STATE', () => { - it('should set the given state', () => { - const state = { - isNotesFetched: false, - }; - - mutations.SET_NOTES_FETCHED_STATE(state, true); - - expect(state.isNotesFetched).toEqual(true); - }); - }); - - describe('SET_DISCUSSION_DIFF_LINES', () => { - it('sets truncated_diff_lines', () => { - const state = { - discussions: [ - { - id: 1, - }, - ], - }; - - mutations.SET_DISCUSSION_DIFF_LINES(state, { - discussionId: 1, - diffLines: [{ text: '+a', rich_text: '+a' }], - }); - - expect(state.discussions[0].truncated_diff_lines).toEqual([{ rich_text: 'a' }]); - }); - - it('keeps reactivity of discussion', () => { - const state = {}; - Vue.set(state, 'discussions', [ - { - id: 1, - expanded: false, - }, - ]); - const discussion = state.discussions[0]; - - mutations.SET_DISCUSSION_DIFF_LINES(state, { - discussionId: 1, - diffLines: [{ rich_text: 'a' }], - }); - - discussion.expanded = true; - - expect(state.discussions[0].expanded).toBe(true); - }); - }); - - describe('DISABLE_COMMENTS', () => { - it('should set comments disabled state', () => { - const state = {}; - - mutations.DISABLE_COMMENTS(state, true); - - expect(state.commentsDisabled).toEqual(true); - }); - }); - - describe('UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS', () => { - it('with unresolvable discussions, updates state', () => { - const state = { - discussions: [ - { individual_note: false, resolvable: true, notes: [UNRESOLVED_NOTE] }, - { individual_note: true, resolvable: true, notes: [UNRESOLVED_NOTE] }, - { individual_note: false, resolvable: false, notes: [UNRESOLVED_NOTE] }, - ], - }; - - mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state); - - expect(state).toEqual( - jasmine.objectContaining({ - resolvableDiscussionsCount: 1, - unresolvedDiscussionsCount: 1, - hasUnresolvedDiscussions: false, - }), - ); - }); - - it('with resolvable discussions, updates state', () => { - const state = { - discussions: [ - { - individual_note: false, - resolvable: true, - notes: [RESOLVED_NOTE, SYSTEM_NOTE, RESOLVED_NOTE], - }, - { - individual_note: false, - resolvable: true, - notes: [RESOLVED_NOTE, SYSTEM_NOTE, WEIRD_NOTE], - }, - { - individual_note: false, - resolvable: true, - notes: [SYSTEM_NOTE, RESOLVED_NOTE, WEIRD_NOTE, UNRESOLVED_NOTE], - }, - { - individual_note: false, - resolvable: true, - notes: [UNRESOLVED_NOTE], - }, - ], - }; - - mutations.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS(state); - - expect(state).toEqual( - jasmine.objectContaining({ - resolvableDiscussionsCount: 4, - unresolvedDiscussionsCount: 2, - hasUnresolvedDiscussions: true, - }), - ); - }); - }); - - describe('CONVERT_TO_DISCUSSION', () => { - let discussion; - let state; - - beforeEach(() => { - discussion = { - id: 42, - individual_note: true, - }; - state = { convertedDisscussionIds: [] }; - }); - - it('adds a discussion to convertedDisscussionIds', () => { - mutations.CONVERT_TO_DISCUSSION(state, discussion.id); - - expect(state.convertedDisscussionIds).toContain(discussion.id); - }); - }); - - describe('REMOVE_CONVERTED_DISCUSSION', () => { - let discussion; - let state; - - beforeEach(() => { - discussion = { - id: 42, - individual_note: true, - }; - state = { convertedDisscussionIds: [41, 42] }; - }); - - it('removes a discussion from convertedDisscussionIds', () => { - mutations.REMOVE_CONVERTED_DISCUSSION(state, discussion.id); - - expect(state.convertedDisscussionIds).not.toContain(discussion.id); - }); - }); -}); diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js deleted file mode 100644 index 5f4dba5ecb9..00000000000 --- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js +++ /dev/null @@ -1,244 +0,0 @@ -import $ from 'jquery'; -import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars -import TimezoneDropdown, { - formatUtcOffset, - formatTimezone, - findTimezoneByIdentifier, -} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown'; - -describe('Timezone Dropdown', function() { - preloadFixtures('pipeline_schedules/edit.html'); - - let $inputEl = null; - let $dropdownEl = null; - let $wrapper = null; - const tzListSel = '.dropdown-content ul li a.is-active'; - const tzDropdownToggleText = '.dropdown-toggle-text'; - - describe('Initialize', () => { - describe('with dropdown already loaded', () => { - beforeEach(() => { - loadFixtures('pipeline_schedules/edit.html'); - $wrapper = $('.dropdown'); - $inputEl = $('#schedule_cron_timezone'); - $dropdownEl = $('.js-timezone-dropdown'); - - // eslint-disable-next-line no-new - new TimezoneDropdown({ - $inputEl, - $dropdownEl, - }); - }); - - it('can take an $inputEl in the constructor', () => { - const tzStr = '[UTC + 5.5] Sri Jayawardenepura'; - const tzValue = 'Asia/Colombo'; - - expect($inputEl.val()).toBe('UTC'); - - $(`${tzListSel}:contains('${tzStr}')`, $wrapper).trigger('click'); - - const val = $inputEl.val(); - - expect(val).toBe(tzValue); - expect(val).not.toBe('UTC'); - }); - - it('will format data array of timezones into a list of offsets', () => { - const data = $dropdownEl.data('data'); - const formatted = $wrapper.find(tzListSel).text(); - - data.forEach(item => { - expect(formatted).toContain(formatTimezone(item)); - }); - }); - - it('will default the timezone to UTC', () => { - const tz = $inputEl.val(); - - expect(tz).toBe('UTC'); - }); - }); - - describe('without dropdown loaded', () => { - beforeEach(() => { - loadFixtures('pipeline_schedules/edit.html'); - $wrapper = $('.dropdown'); - $inputEl = $('#schedule_cron_timezone'); - $dropdownEl = $('.js-timezone-dropdown'); - }); - - it('will populate the list of UTC offsets after the dropdown is loaded', () => { - expect($wrapper.find(tzListSel).length).toEqual(0); - - // eslint-disable-next-line no-new - new TimezoneDropdown({ - $inputEl, - $dropdownEl, - }); - - expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length); - }); - - it('will call a provided handler when a new timezone is selected', () => { - const onSelectTimezone = jasmine.createSpy('onSelectTimezoneMock'); - // eslint-disable-next-line no-new - new TimezoneDropdown({ - $inputEl, - $dropdownEl, - onSelectTimezone, - }); - - $wrapper - .find(tzListSel) - .first() - .trigger('click'); - - expect(onSelectTimezone).toHaveBeenCalled(); - }); - - it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => { - $inputEl.val('America/St_Johns'); - - // eslint-disable-next-line no-new - new TimezoneDropdown({ - $inputEl, - $dropdownEl, - displayFormat: selectedItem => formatTimezone(selectedItem), - }); - - expect($wrapper.find(tzDropdownToggleText).html()).toEqual('[UTC - 2.5] Newfoundland'); - }); - - it('will call a provided `displayFormat` handler to format the dropdown value', () => { - const displayFormat = jasmine.createSpy('displayFormat'); - // eslint-disable-next-line no-new - new TimezoneDropdown({ - $inputEl, - $dropdownEl, - displayFormat, - }); - - $wrapper - .find(tzListSel) - .first() - .trigger('click'); - - expect(displayFormat).toHaveBeenCalled(); - }); - }); - }); - - describe('formatUtcOffset', () => { - it('will convert negative utc offsets in seconds to hours and minutes', () => { - expect(formatUtcOffset(-21600)).toEqual('- 6'); - }); - - it('will convert positive utc offsets in seconds to hours and minutes', () => { - expect(formatUtcOffset(25200)).toEqual('+ 7'); - expect(formatUtcOffset(49500)).toEqual('+ 13.75'); - }); - - it('will return 0 when given a string', () => { - expect(formatUtcOffset('BLAH')).toEqual('0'); - expect(formatUtcOffset('$%$%')).toEqual('0'); - }); - - it('will return 0 when given an array', () => { - expect(formatUtcOffset(['an', 'array'])).toEqual('0'); - }); - - it('will return 0 when given an object', () => { - expect(formatUtcOffset({ some: '', object: '' })).toEqual('0'); - }); - - it('will return 0 when given null', () => { - expect(formatUtcOffset(null)).toEqual('0'); - }); - - it('will return 0 when given undefined', () => { - expect(formatUtcOffset(undefined)).toEqual('0'); - }); - - it('will return 0 when given empty input', () => { - expect(formatUtcOffset('')).toEqual('0'); - }); - }); - - describe('formatTimezone', () => { - it('given name: "Chatham Is.", offset: "49500", will format for display as "[UTC + 13.75] Chatham Is."', () => { - expect( - formatTimezone({ - name: 'Chatham Is.', - offset: 49500, - identifier: 'Pacific/Chatham', - }), - ).toEqual('[UTC + 13.75] Chatham Is.'); - }); - - it('given name: "Saskatchewan", offset: "-21600", will format for display as "[UTC - 6] Saskatchewan"', () => { - expect( - formatTimezone({ - name: 'Saskatchewan', - offset: -21600, - identifier: 'America/Regina', - }), - ).toEqual('[UTC - 6] Saskatchewan'); - }); - - it('given name: "Accra", offset: "0", will format for display as "[UTC 0] Accra"', () => { - expect( - formatTimezone({ - name: 'Accra', - offset: 0, - identifier: 'Africa/Accra', - }), - ).toEqual('[UTC 0] Accra'); - }); - }); - - describe('findTimezoneByIdentifier', () => { - const tzList = [ - { - identifier: 'Asia/Tokyo', - name: 'Sapporo', - offset: 32400, - }, - { - identifier: 'Asia/Hong_Kong', - name: 'Hong Kong', - offset: 28800, - }, - { - identifier: 'Asia/Dhaka', - name: 'Dhaka', - offset: 21600, - }, - ]; - - const identifier = 'Asia/Dhaka'; - it('returns the correct object if the identifier exists', () => { - const res = findTimezoneByIdentifier(tzList, identifier); - - expect(res).toBeTruthy(); - expect(res).toBe(tzList[2]); - }); - - it('returns null if it doesnt find the identifier', () => { - const res = findTimezoneByIdentifier(tzList, 'Australia/Melbourne'); - - expect(res).toBeNull(); - }); - - it('returns null if there is no identifier given', () => { - expect(findTimezoneByIdentifier(tzList)).toBeNull(); - expect(findTimezoneByIdentifier(tzList, '')).toBeNull(); - }); - - it('returns null if there is an empty or invalid array given', () => { - expect(findTimezoneByIdentifier([], identifier)).toBeNull(); - expect(findTimezoneByIdentifier(null, identifier)).toBeNull(); - expect(findTimezoneByIdentifier(undefined, identifier)).toBeNull(); - }); - }); -}); diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js deleted file mode 100644 index 7806cdf1477..00000000000 --- a/spec/javascripts/pipelines/nav_controls_spec.js +++ /dev/null @@ -1,85 +0,0 @@ -import Vue from 'vue'; -import navControlsComp from '~/pipelines/components/nav_controls.vue'; -import mountComponent from '../helpers/vue_mount_component_helper'; - -describe('Pipelines Nav Controls', () => { - let NavControlsComponent; - let component; - - beforeEach(() => { - NavControlsComponent = Vue.extend(navControlsComp); - }); - - afterEach(() => { - component.$destroy(); - }); - - it('should render link to create a new pipeline', () => { - const mockData = { - newPipelinePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - }; - - component = mountComponent(NavControlsComponent, mockData); - - expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline'); - expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual( - mockData.newPipelinePath, - ); - }); - - it('should not render link to create pipeline if no path is provided', () => { - const mockData = { - helpPagePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - }; - - component = mountComponent(NavControlsComponent, mockData); - - expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null); - }); - - it('should render link for CI lint', () => { - const mockData = { - newPipelinePath: 'foo', - helpPagePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - }; - - component = mountComponent(NavControlsComponent, mockData); - - expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint'); - expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual( - mockData.ciLintPath, - ); - }); - - describe('Reset Runners Cache', () => { - beforeEach(() => { - const mockData = { - newPipelinePath: 'foo', - ciLintPath: 'foo', - resetCachePath: 'foo', - }; - - component = mountComponent(NavControlsComponent, mockData); - }); - - it('should render button for resetting runner caches', () => { - expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain( - 'Clear Runner Caches', - ); - }); - - it('should emit postAction event when reset runner cache button is clicked', () => { - spyOn(component, '$emit'); - - component.$el.querySelector('.js-clear-cache').click(); - - expect(component.$emit).toHaveBeenCalledWith('resetRunnersCache', 'foo'); - }); - }); -}); diff --git a/spec/javascripts/polyfills/element_spec.js b/spec/javascripts/polyfills/element_spec.js deleted file mode 100644 index d35df595c72..00000000000 --- a/spec/javascripts/polyfills/element_spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import '~/commons/polyfills/element'; - -describe('Element polyfills', function() { - beforeEach(() => { - this.element = document.createElement('ul'); - }); - - describe('matches', () => { - it('returns true if element matches the selector', () => { - expect(this.element.matches('ul')).toBeTruthy(); - }); - - it("returns false if element doesn't match the selector", () => { - expect(this.element.matches('.not-an-element')).toBeFalsy(); - }); - }); - - describe('closest', () => { - beforeEach(() => { - this.childElement = document.createElement('li'); - this.element.appendChild(this.childElement); - }); - - it('returns the closest parent that matches the selector', () => { - expect(this.childElement.closest('ul').toString()).toBe(this.element.toString()); - }); - - it('returns itself if it matches the selector', () => { - expect(this.childElement.closest('li').toString()).toBe(this.childElement.toString()); - }); - - it('returns undefined if nothing matches the selector', () => { - expect(this.childElement.closest('.no-an-element')).toBeFalsy(); - }); - }); -}); diff --git a/spec/javascripts/profile/add_ssh_key_validation_spec.js b/spec/javascripts/profile/add_ssh_key_validation_spec.js deleted file mode 100644 index c71a2885acc..00000000000 --- a/spec/javascripts/profile/add_ssh_key_validation_spec.js +++ /dev/null @@ -1,69 +0,0 @@ -import AddSshKeyValidation from '../../../app/assets/javascripts/profile/add_ssh_key_validation'; - -describe('AddSshKeyValidation', () => { - describe('submit', () => { - it('returns true if isValid is true', () => { - const addSshKeyValidation = new AddSshKeyValidation({}); - spyOn(AddSshKeyValidation, 'isPublicKey').and.returnValue(true); - - expect(addSshKeyValidation.submit()).toBeTruthy(); - }); - - it('calls preventDefault and toggleWarning if isValid is false', () => { - const addSshKeyValidation = new AddSshKeyValidation({}); - const event = jasmine.createSpyObj('event', ['preventDefault']); - spyOn(AddSshKeyValidation, 'isPublicKey').and.returnValue(false); - spyOn(addSshKeyValidation, 'toggleWarning'); - - addSshKeyValidation.submit(event); - - expect(event.preventDefault).toHaveBeenCalled(); - expect(addSshKeyValidation.toggleWarning).toHaveBeenCalledWith(true); - }); - }); - - describe('toggleWarning', () => { - it('shows warningElement and hides originalSubmitElement if isVisible is true', () => { - const warningElement = document.createElement('div'); - const originalSubmitElement = document.createElement('div'); - warningElement.classList.add('hide'); - - const addSshKeyValidation = new AddSshKeyValidation( - {}, - warningElement, - originalSubmitElement, - ); - addSshKeyValidation.toggleWarning(true); - - expect(warningElement.classList.contains('hide')).toBeFalsy(); - expect(originalSubmitElement.classList.contains('hide')).toBeTruthy(); - }); - - it('hides warningElement and shows originalSubmitElement if isVisible is false', () => { - const warningElement = document.createElement('div'); - const originalSubmitElement = document.createElement('div'); - originalSubmitElement.classList.add('hide'); - - const addSshKeyValidation = new AddSshKeyValidation( - {}, - warningElement, - originalSubmitElement, - ); - addSshKeyValidation.toggleWarning(false); - - expect(warningElement.classList.contains('hide')).toBeTruthy(); - expect(originalSubmitElement.classList.contains('hide')).toBeFalsy(); - }); - }); - - describe('isPublicKey', () => { - it('returns false if probably invalid public ssh key', () => { - expect(AddSshKeyValidation.isPublicKey('nope')).toBeFalsy(); - }); - - it('returns true if probably valid public ssh key', () => { - expect(AddSshKeyValidation.isPublicKey('ssh-')).toBeTruthy(); - expect(AddSshKeyValidation.isPublicKey('ecdsa-sha2-')).toBeTruthy(); - }); - }); -}); diff --git a/spec/javascripts/project_select_combo_button_spec.js b/spec/javascripts/project_select_combo_button_spec.js deleted file mode 100644 index dc85292c23e..00000000000 --- a/spec/javascripts/project_select_combo_button_spec.js +++ /dev/null @@ -1,124 +0,0 @@ -import $ from 'jquery'; -import ProjectSelectComboButton from '~/project_select_combo_button'; - -const fixturePath = 'static/project_select_combo_button.html'; - -describe('Project Select Combo Button', function() { - preloadFixtures(fixturePath); - - beforeEach(function() { - this.defaults = { - label: 'Select project to create issue', - groupId: 12345, - projectMeta: { - name: 'My Cool Project', - url: 'http://mycoolproject.com', - }, - newProjectMeta: { - name: 'My Other Cool Project', - url: 'http://myothercoolproject.com', - }, - localStorageKey: 'group-12345-new-issue-recent-project', - relativePath: 'issues/new', - }; - - loadFixtures(fixturePath); - - this.newItemBtn = document.querySelector('.new-project-item-link'); - this.projectSelectInput = document.querySelector('.project-item-select'); - }); - - describe('on page load when localStorage is empty', function() { - beforeEach(function() { - this.comboButton = new ProjectSelectComboButton(this.projectSelectInput); - }); - - it('newItemBtn href is null', function() { - expect(this.newItemBtn.getAttribute('href')).toBe(''); - }); - - it('newItemBtn text is the plain default label', function() { - expect(this.newItemBtn.textContent).toBe(this.defaults.label); - }); - }); - - describe('on page load when localStorage is filled', function() { - beforeEach(function() { - window.localStorage.setItem( - this.defaults.localStorageKey, - JSON.stringify(this.defaults.projectMeta), - ); - this.comboButton = new ProjectSelectComboButton(this.projectSelectInput); - }); - - it('newItemBtn href is correctly set', function() { - expect(this.newItemBtn.getAttribute('href')).toBe(this.defaults.projectMeta.url); - }); - - it('newItemBtn text is the cached label', function() { - expect(this.newItemBtn.textContent).toBe(`New issue in ${this.defaults.projectMeta.name}`); - }); - - afterEach(function() { - window.localStorage.clear(); - }); - }); - - describe('after selecting a new project', function() { - beforeEach(function() { - this.comboButton = new ProjectSelectComboButton(this.projectSelectInput); - - // mock the effect of selecting an item from the projects dropdown (select2) - $('.project-item-select') - .val(JSON.stringify(this.defaults.newProjectMeta)) - .trigger('change'); - }); - - it('newItemBtn href is correctly set', function() { - expect(this.newItemBtn.getAttribute('href')).toBe('http://myothercoolproject.com/issues/new'); - }); - - it('newItemBtn text is the selected project label', function() { - expect(this.newItemBtn.textContent).toBe(`New issue in ${this.defaults.newProjectMeta.name}`); - }); - - afterEach(function() { - window.localStorage.clear(); - }); - }); - - describe('deriveTextVariants', function() { - beforeEach(function() { - this.mockExecutionContext = { - resourceType: '', - resourceLabel: '', - }; - - this.comboButton = new ProjectSelectComboButton(this.projectSelectInput); - - this.method = this.comboButton.deriveTextVariants.bind(this.mockExecutionContext); - }); - - it('correctly derives test variants for merge requests', function() { - this.mockExecutionContext.resourceType = 'merge_requests'; - this.mockExecutionContext.resourceLabel = 'New merge request'; - - const returnedVariants = this.method(); - - expect(returnedVariants.localStorageItemType).toBe('new-merge-request'); - expect(returnedVariants.defaultTextPrefix).toBe('New merge request'); - expect(returnedVariants.presetTextSuffix).toBe('merge request'); - }); - - it('correctly derives text variants for issues', function() { - this.mockExecutionContext.resourceType = 'issues'; - this.mockExecutionContext.resourceLabel = 'New issue'; - - const returnedVariants = this.method(); - - expect(returnedVariants.localStorageItemType).toBe('new-issue'); - expect(returnedVariants.defaultTextPrefix).toBe('New issue'); - expect(returnedVariants.presetTextSuffix).toBe('issue'); - }); - }); -}); diff --git a/spec/javascripts/shared/popover_spec.js b/spec/javascripts/shared/popover_spec.js deleted file mode 100644 index cc2b2014d38..00000000000 --- a/spec/javascripts/shared/popover_spec.js +++ /dev/null @@ -1,166 +0,0 @@ -import $ from 'jquery'; -import { togglePopover, mouseleave, mouseenter } from '~/shared/popover'; - -describe('popover', () => { - describe('togglePopover', () => { - describe('togglePopover(true)', () => { - it('returns true when popover is shown', () => { - const context = { - hasClass: () => false, - popover: () => {}, - toggleClass: () => {}, - }; - - expect(togglePopover.call(context, true)).toEqual(true); - }); - - it('returns false when popover is already shown', () => { - const context = { - hasClass: () => true, - }; - - expect(togglePopover.call(context, true)).toEqual(false); - }); - - it('shows popover', done => { - const context = { - hasClass: () => false, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'popover').and.callFake(method => { - expect(method).toEqual('show'); - done(); - }); - - togglePopover.call(context, true); - }); - - it('adds disable-animation and js-popover-show class', done => { - const context = { - hasClass: () => false, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'toggleClass').and.callFake((classNames, show) => { - expect(classNames).toEqual('disable-animation js-popover-show'); - expect(show).toEqual(true); - done(); - }); - - togglePopover.call(context, true); - }); - }); - - describe('togglePopover(false)', () => { - it('returns true when popover is hidden', () => { - const context = { - hasClass: () => true, - popover: () => {}, - toggleClass: () => {}, - }; - - expect(togglePopover.call(context, false)).toEqual(true); - }); - - it('returns false when popover is already hidden', () => { - const context = { - hasClass: () => false, - }; - - expect(togglePopover.call(context, false)).toEqual(false); - }); - - it('hides popover', done => { - const context = { - hasClass: () => true, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'popover').and.callFake(method => { - expect(method).toEqual('hide'); - done(); - }); - - togglePopover.call(context, false); - }); - - it('removes disable-animation and js-popover-show class', done => { - const context = { - hasClass: () => true, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'toggleClass').and.callFake((classNames, show) => { - expect(classNames).toEqual('disable-animation js-popover-show'); - expect(show).toEqual(false); - done(); - }); - - togglePopover.call(context, false); - }); - }); - }); - - describe('mouseleave', () => { - it('calls hide popover if .popover:hover is false', () => { - const fakeJquery = { - length: 0, - }; - - spyOn($.fn, 'init').and.callFake(selector => - selector === '.popover:hover' ? fakeJquery : $.fn, - ); - spyOn(togglePopover, 'call'); - mouseleave(); - - expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false); - }); - - it('does not call hide popover if .popover:hover is true', () => { - const fakeJquery = { - length: 1, - }; - - spyOn($.fn, 'init').and.callFake(selector => - selector === '.popover:hover' ? fakeJquery : $.fn, - ); - spyOn(togglePopover, 'call'); - mouseleave(); - - expect(togglePopover.call).not.toHaveBeenCalledWith(false); - }); - }); - - describe('mouseenter', () => { - const context = {}; - - it('shows popover', () => { - spyOn(togglePopover, 'call').and.returnValue(false); - mouseenter.call(context); - - expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true); - }); - - it('registers mouseleave event if popover is showed', done => { - spyOn(togglePopover, 'call').and.returnValue(true); - spyOn($.fn, 'on').and.callFake(eventName => { - expect(eventName).toEqual('mouseleave'); - done(); - }); - mouseenter.call(context); - }); - - it('does not register mouseleave event if popover is not showed', () => { - spyOn(togglePopover, 'call').and.returnValue(false); - const spy = spyOn($.fn, 'on').and.callFake(() => {}); - mouseenter.call(context); - - expect(spy).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js deleted file mode 100644 index 85ff70fffbd..00000000000 --- a/spec/javascripts/sidebar/sidebar_store_spec.js +++ /dev/null @@ -1,162 +0,0 @@ -import SidebarStore from '~/sidebar/stores/sidebar_store'; -import Mock from './mock_data'; -import UsersMockHelper from '../helpers/user_mock_data_helper'; - -const ASSIGNEE = { - id: 2, - name: 'gitlab user 2', - username: 'gitlab2', - avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', -}; - -const ANOTHER_ASSINEE = { - id: 3, - name: 'gitlab user 3', - username: 'gitlab3', - avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', -}; - -const PARTICIPANT = { - id: 1, - state: 'active', - username: 'marcene', - name: 'Allie Will', - web_url: 'foo.com', - avatar_url: 'gravatar.com/avatar/xxx', -}; - -const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }]; - -describe('Sidebar store', function() { - beforeEach(() => { - this.store = new SidebarStore({ - currentUser: { - id: 1, - name: 'Administrator', - username: 'root', - avatar_url: - 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - }, - editable: true, - rootPath: '/', - endpoint: '/gitlab-org/gitlab-shell/issues/5.json', - }); - }); - - afterEach(() => { - SidebarStore.singleton = null; - }); - - it('has default isFetching values', () => { - expect(this.store.isFetching.assignees).toBe(true); - }); - - it('adds a new assignee', () => { - this.store.addAssignee(ASSIGNEE); - - expect(this.store.assignees.length).toEqual(1); - }); - - it('removes an assignee', () => { - this.store.removeAssignee(ASSIGNEE); - - expect(this.store.assignees.length).toEqual(0); - }); - - it('finds an existent assignee', () => { - let foundAssignee; - - this.store.addAssignee(ASSIGNEE); - foundAssignee = this.store.findAssignee(ASSIGNEE); - - expect(foundAssignee).toBeDefined(); - expect(foundAssignee).toEqual(ASSIGNEE); - foundAssignee = this.store.findAssignee(ANOTHER_ASSINEE); - - expect(foundAssignee).toBeUndefined(); - }); - - it('removes all assignees', () => { - this.store.removeAllAssignees(); - - expect(this.store.assignees.length).toEqual(0); - }); - - it('sets participants data', () => { - expect(this.store.participants.length).toEqual(0); - - this.store.setParticipantsData({ - participants: PARTICIPANT_LIST, - }); - - expect(this.store.isFetching.participants).toEqual(false); - expect(this.store.participants.length).toEqual(PARTICIPANT_LIST.length); - }); - - it('sets subcriptions data', () => { - expect(this.store.subscribed).toEqual(null); - - this.store.setSubscriptionsData({ - subscribed: true, - }); - - expect(this.store.isFetching.subscriptions).toEqual(false); - expect(this.store.subscribed).toEqual(true); - }); - - it('set assigned data', () => { - const users = { - assignees: UsersMockHelper.createNumberRandomUsers(3), - }; - - this.store.setAssigneeData(users); - - expect(this.store.isFetching.assignees).toBe(false); - expect(this.store.assignees.length).toEqual(3); - }); - - it('sets fetching state', () => { - expect(this.store.isFetching.participants).toEqual(true); - - this.store.setFetchingState('participants', false); - - expect(this.store.isFetching.participants).toEqual(false); - }); - - it('sets loading state', () => { - this.store.setLoadingState('assignees', true); - - expect(this.store.isLoading.assignees).toEqual(true); - }); - - it('set time tracking data', () => { - this.store.setTimeTrackingData(Mock.time); - - expect(this.store.timeEstimate).toEqual(Mock.time.time_estimate); - expect(this.store.totalTimeSpent).toEqual(Mock.time.total_time_spent); - expect(this.store.humanTimeEstimate).toEqual(Mock.time.human_time_estimate); - expect(this.store.humanTotalTimeSpent).toEqual(Mock.time.human_total_time_spent); - }); - - it('set autocomplete projects', () => { - const projects = [{ id: 0 }]; - this.store.setAutocompleteProjects(projects); - - expect(this.store.autocompleteProjects).toEqual(projects); - }); - - it('sets subscribed state', () => { - expect(this.store.subscribed).toEqual(null); - - this.store.setSubscribedState(true); - - expect(this.store.subscribed).toEqual(true); - }); - - it('set move to project ID', () => { - const projectId = 7; - this.store.setMoveToProjectId(projectId); - - expect(this.store.moveToProjectId).toEqual(projectId); - }); -}); diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js deleted file mode 100644 index 99c47fa31d4..00000000000 --- a/spec/javascripts/syntax_highlight_spec.js +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable no-return-assign */ - -import $ from 'jquery'; -import syntaxHighlight from '~/syntax_highlight'; - -describe('Syntax Highlighter', function() { - const stubUserColorScheme = function(value) { - if (window.gon == null) { - window.gon = {}; - } - return (window.gon.user_color_scheme = value); - }; - describe('on a js-syntax-highlight element', function() { - beforeEach(function() { - return setFixtures('
'); - }); - - it('applies syntax highlighting', function() { - stubUserColorScheme('monokai'); - syntaxHighlight($('.js-syntax-highlight')); - - expect($('.js-syntax-highlight')).toHaveClass('monokai'); - }); - }); - - describe('on a parent element', function() { - beforeEach(function() { - return setFixtures( - '
\n
\n
\n
\n
', - ); - }); - - it('applies highlighting to all applicable children', function() { - stubUserColorScheme('monokai'); - syntaxHighlight($('.parent')); - - expect($('.parent, .foo')).not.toHaveClass('monokai'); - expect($('.monokai').length).toBe(2); - }); - - it('prevents an infinite loop when no matches exist', function() { - setFixtures('
'); - const highlight = function() { - return syntaxHighlight($('div')); - }; - - expect(highlight).not.toThrow(); - }); - }); -}); diff --git a/spec/javascripts/task_list_spec.js b/spec/javascripts/task_list_spec.js deleted file mode 100644 index 563f402de58..00000000000 --- a/spec/javascripts/task_list_spec.js +++ /dev/null @@ -1,156 +0,0 @@ -import $ from 'jquery'; -import TaskList from '~/task_list'; -import axios from '~/lib/utils/axios_utils'; - -describe('TaskList', () => { - let taskList; - let currentTarget; - const taskListOptions = { - selector: '.task-list', - dataType: 'issue', - fieldName: 'description', - lockVersion: 2, - }; - const createTaskList = () => new TaskList(taskListOptions); - - beforeEach(() => { - setFixtures(` -
-
-
- `); - - currentTarget = $('
'); - taskList = createTaskList(); - }); - - it('should call init when the class constructed', () => { - spyOn(TaskList.prototype, 'init').and.callThrough(); - spyOn(TaskList.prototype, 'disable'); - spyOn($.prototype, 'taskList'); - spyOn($.prototype, 'on'); - - taskList = createTaskList(); - const $taskListEl = $(taskList.taskListContainerSelector); - - expect(taskList.init).toHaveBeenCalled(); - expect(taskList.disable).toHaveBeenCalled(); - expect($taskListEl.taskList).toHaveBeenCalledWith('enable'); - expect($(document).on).toHaveBeenCalledWith( - 'tasklist:changed', - taskList.taskListContainerSelector, - taskList.updateHandler, - ); - }); - - describe('getTaskListTarget', () => { - it('should return currentTarget from event object if exists', () => { - const $target = taskList.getTaskListTarget({ currentTarget }); - - expect($target).toEqual(currentTarget); - }); - - it('should return element of the taskListContainerSelector', () => { - const $target = taskList.getTaskListTarget(); - - expect($target).toEqual($(taskList.taskListContainerSelector)); - }); - }); - - describe('disableTaskListItems', () => { - it('should call taskList method with disable param', () => { - spyOn($.prototype, 'taskList'); - - taskList.disableTaskListItems({ currentTarget }); - - expect(currentTarget.taskList).toHaveBeenCalledWith('disable'); - }); - }); - - describe('enableTaskListItems', () => { - it('should call taskList method with enable param', () => { - spyOn($.prototype, 'taskList'); - - taskList.enableTaskListItems({ currentTarget }); - - expect(currentTarget.taskList).toHaveBeenCalledWith('enable'); - }); - }); - - describe('disable', () => { - it('should disable task list items and off document event', () => { - spyOn(taskList, 'disableTaskListItems'); - spyOn($.prototype, 'off'); - - taskList.disable(); - - expect(taskList.disableTaskListItems).toHaveBeenCalled(); - expect($(document).off).toHaveBeenCalledWith( - 'tasklist:changed', - taskList.taskListContainerSelector, - ); - }); - }); - - describe('update', () => { - it('should disable task list items and make a patch request then enable them again', done => { - const response = { data: { lock_version: 3 } }; - spyOn(taskList, 'enableTaskListItems'); - spyOn(taskList, 'disableTaskListItems'); - spyOn(taskList, 'onSuccess'); - spyOn(axios, 'patch').and.returnValue(Promise.resolve(response)); - - const value = 'hello world'; - const endpoint = '/foo'; - const target = $(``); - const detail = { - index: 2, - checked: true, - lineNumber: 8, - lineSource: '- [ ] check item', - }; - const event = { target, detail }; - const patchData = { - [taskListOptions.dataType]: { - [taskListOptions.fieldName]: value, - lock_version: taskListOptions.lockVersion, - update_task: { - index: detail.index, - checked: detail.checked, - line_number: detail.lineNumber, - line_source: detail.lineSource, - }, - }, - }; - - taskList - .update(event) - .then(() => { - expect(taskList.disableTaskListItems).toHaveBeenCalledWith(event); - expect(axios.patch).toHaveBeenCalledWith(endpoint, patchData); - expect(taskList.enableTaskListItems).toHaveBeenCalledWith(event); - expect(taskList.onSuccess).toHaveBeenCalledWith(response.data); - expect(taskList.lockVersion).toEqual(response.data.lock_version); - }) - .then(done) - .catch(done.fail); - }); - }); - - it('should handle request error and enable task list items', done => { - const response = { data: { error: 1 } }; - spyOn(taskList, 'enableTaskListItems'); - spyOn(taskList, 'onError'); - spyOn(axios, 'patch').and.returnValue(Promise.reject({ response })); // eslint-disable-line prefer-promise-reject-errors - - const event = { detail: {} }; - taskList - .update(event) - .then(() => { - expect(taskList.enableTaskListItems).toHaveBeenCalledWith(event); - expect(taskList.onError).toHaveBeenCalledWith(response.data); - }) - .then(done) - .catch(done.fail); - }); -}); diff --git a/spec/javascripts/version_check_image_spec.js b/spec/javascripts/version_check_image_spec.js deleted file mode 100644 index 0e69fcc4c5f..00000000000 --- a/spec/javascripts/version_check_image_spec.js +++ /dev/null @@ -1,35 +0,0 @@ -import $ from 'jquery'; -import VersionCheckImage from '~/version_check_image'; -import ClassSpecHelper from './helpers/class_spec_helper'; - -describe('VersionCheckImage', function() { - describe('bindErrorEvent', function() { - ClassSpecHelper.itShouldBeAStaticMethod(VersionCheckImage, 'bindErrorEvent'); - - beforeEach(function() { - this.imageElement = $('
'); - }); - - it('registers an error event', function() { - spyOn($.prototype, 'on'); - spyOn($.prototype, 'off').and.callFake(function() { - return this; - }); - - VersionCheckImage.bindErrorEvent(this.imageElement); - - expect($.prototype.off).toHaveBeenCalledWith('error'); - expect($.prototype.on).toHaveBeenCalledWith('error', jasmine.any(Function)); - }); - - it('hides the imageElement on error', function() { - spyOn($.prototype, 'hide'); - - VersionCheckImage.bindErrorEvent(this.imageElement); - - this.imageElement.trigger('error'); - - expect($.prototype.hide).toHaveBeenCalled(); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/gl_modal_vuex_spec.js b/spec/javascripts/vue_shared/components/gl_modal_vuex_spec.js deleted file mode 100644 index eb78d37db3e..00000000000 --- a/spec/javascripts/vue_shared/components/gl_modal_vuex_spec.js +++ /dev/null @@ -1,151 +0,0 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import Vuex from 'vuex'; -import { GlModal } from '@gitlab/ui'; -import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue'; -import createState from '~/vuex_shared/modules/modal/state'; - -const localVue = createLocalVue(); -localVue.use(Vuex); - -const TEST_SLOT = 'Lorem ipsum modal dolar sit.'; -const TEST_MODAL_ID = 'my-modal-id'; -const TEST_MODULE = 'myModal'; - -describe('GlModalVuex', () => { - let wrapper; - let state; - let actions; - - const factory = (options = {}) => { - const store = new Vuex.Store({ - modules: { - [TEST_MODULE]: { - namespaced: true, - state, - actions, - }, - }, - }); - - const propsData = { - modalId: TEST_MODAL_ID, - modalModule: TEST_MODULE, - ...options.propsData, - }; - - wrapper = shallowMount(localVue.extend(GlModalVuex), { - ...options, - localVue, - store, - propsData, - }); - }; - - beforeEach(() => { - state = createState(); - - actions = { - show: jasmine.createSpy('show'), - hide: jasmine.createSpy('hide'), - }; - }); - - it('renders gl-modal', () => { - factory({ - slots: { - default: `
${TEST_SLOT}
`, - }, - }); - const glModal = wrapper.find(GlModal); - - expect(glModal.props('modalId')).toBe(TEST_MODAL_ID); - expect(glModal.text()).toContain(TEST_SLOT); - }); - - it('passes props through to gl-modal', () => { - const title = 'Test Title'; - const okVariant = 'success'; - - factory({ - propsData: { - title, - okTitle: title, - okVariant, - }, - }); - const glModal = wrapper.find(GlModal); - - expect(glModal.attributes('title')).toEqual(title); - expect(glModal.attributes('oktitle')).toEqual(title); - expect(glModal.attributes('okvariant')).toEqual(okVariant); - }); - - it('passes listeners through to gl-modal', () => { - const ok = jasmine.createSpy('ok'); - - factory({ - listeners: { ok }, - }); - - const glModal = wrapper.find(GlModal); - glModal.vm.$emit('ok'); - - expect(ok).toHaveBeenCalledTimes(1); - }); - - it('calls vuex action on show', () => { - expect(actions.show).not.toHaveBeenCalled(); - - factory(); - - const glModal = wrapper.find(GlModal); - glModal.vm.$emit('shown'); - - expect(actions.show).toHaveBeenCalledTimes(1); - }); - - it('calls vuex action on hide', () => { - expect(actions.hide).not.toHaveBeenCalled(); - - factory(); - - const glModal = wrapper.find(GlModal); - glModal.vm.$emit('hidden'); - - expect(actions.hide).toHaveBeenCalledTimes(1); - }); - - it('calls bootstrap show when isVisible changes', done => { - state.isVisible = false; - - factory(); - const rootEmit = spyOn(wrapper.vm.$root, '$emit'); - - state.isVisible = true; - - localVue - .nextTick() - .then(() => { - expect(rootEmit).toHaveBeenCalledWith('bv::show::modal', TEST_MODAL_ID); - }) - .then(done) - .catch(done.fail); - }); - - it('calls bootstrap hide when isVisible changes', done => { - state.isVisible = true; - - factory(); - const rootEmit = spyOn(wrapper.vm.$root, '$emit'); - - state.isVisible = false; - - localVue - .nextTick() - .then(() => { - expect(rootEmit).toHaveBeenCalledWith('bv::hide::modal', TEST_MODAL_ID); - }) - .then(done) - .catch(done.fail); - }); -}); diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js deleted file mode 100644 index dc929e83eb7..00000000000 --- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js +++ /dev/null @@ -1,101 +0,0 @@ -import Vue from 'vue'; -import SuggestionDiffComponent from '~/vue_shared/components/markdown/suggestion_diff.vue'; -import { selectDiffLines } from '~/vue_shared/components/lib/utils/diff_utils'; - -const MOCK_DATA = { - canApply: true, - suggestion: { - id: 1, - diff_lines: [ - { - can_receive_suggestion: false, - line_code: null, - meta_data: null, - new_line: null, - old_line: 5, - rich_text: '-test', - text: '-test', - type: 'old', - }, - { - can_receive_suggestion: true, - line_code: null, - meta_data: null, - new_line: 5, - old_line: null, - rich_text: '+new test', - text: '+new test', - type: 'new', - }, - { - can_receive_suggestion: true, - line_code: null, - meta_data: null, - new_line: 5, - old_line: null, - rich_text: '+new test2', - text: '+new test2', - type: 'new', - }, - ], - }, - helpPagePath: 'path_to_docs', -}; - -const lines = selectDiffLines(MOCK_DATA.suggestion.diff_lines); -const newLines = lines.filter(line => line.type === 'new'); - -describe('Suggestion Diff component', () => { - let vm; - - beforeEach(done => { - const Component = Vue.extend(SuggestionDiffComponent); - - vm = new Component({ - propsData: MOCK_DATA, - }).$mount(); - - Vue.nextTick(done); - }); - - describe('init', () => { - it('renders a suggestion header', () => { - expect(vm.$el.querySelector('.js-suggestion-diff-header')).not.toBeNull(); - }); - - it('renders a diff table with syntax highlighting', () => { - expect(vm.$el.querySelector('.md-suggestion-diff.js-syntax-highlight.code')).not.toBeNull(); - }); - - it('renders the oldLineNumber', () => { - const fromLine = vm.$el.querySelector('.old_line').innerHTML; - - expect(parseInt(fromLine, 10)).toBe(lines[0].old_line); - }); - - it('renders the oldLineContent', () => { - const fromContent = vm.$el.querySelector('.line_content.old').innerHTML; - - expect(fromContent.includes(lines[0].text)).toBe(true); - }); - - it('renders new lines', () => { - const newLinesElements = vm.$el.querySelectorAll('.line_holder.new'); - - newLinesElements.forEach((line, i) => { - expect(newLinesElements[i].innerHTML.includes(newLines[i].new_line)).toBe(true); - expect(newLinesElements[i].innerHTML.includes(newLines[i].text)).toBe(true); - }); - }); - }); - - describe('applySuggestion', () => { - it('emits apply event when applySuggestion is called', () => { - const callback = () => {}; - spyOn(vm, '$emit'); - vm.applySuggestion(callback); - - expect(vm.$emit).toHaveBeenCalledWith('apply', { suggestionId: vm.suggestion.id, callback }); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js deleted file mode 100644 index 96bc3b0cc17..00000000000 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_list_spec.js +++ /dev/null @@ -1,156 +0,0 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { GlButton } from '@gitlab/ui'; -import { TEST_HOST } from 'spec/test_constants'; -import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue'; -import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; - -const TEST_IMAGE_SIZE = 7; -const TEST_BREAKPOINT = 5; -const TEST_EMPTY_MESSAGE = 'Lorem ipsum empty'; -const DEFAULT_EMPTY_MESSAGE = 'None'; - -const createUser = id => ({ - id, - name: 'Lorem', - web_url: `${TEST_HOST}/${id}`, - avatar_url: `${TEST_HOST}/${id}/avatar`, -}); -const createList = n => - Array(n) - .fill(1) - .map((x, id) => createUser(id)); - -const localVue = createLocalVue(); - -describe('UserAvatarList', () => { - let props; - let wrapper; - - const factory = (options = {}) => { - const propsData = { - ...props, - ...options.propsData, - }; - - wrapper = shallowMount(localVue.extend(UserAvatarList), { - ...options, - localVue, - propsData, - }); - }; - - const clickButton = () => { - const button = wrapper.find(GlButton); - button.vm.$emit('click'); - }; - - beforeEach(() => { - props = { imgSize: TEST_IMAGE_SIZE }; - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('empty text', () => { - it('shows when items are empty', () => { - factory({ propsData: { items: [] } }); - - expect(wrapper.text()).toContain(DEFAULT_EMPTY_MESSAGE); - }); - - it('does not show when items are not empty', () => { - factory({ propsData: { items: createList(1) } }); - - expect(wrapper.text()).not.toContain(DEFAULT_EMPTY_MESSAGE); - }); - - it('can be set in props', () => { - factory({ propsData: { items: [], emptyText: TEST_EMPTY_MESSAGE } }); - - expect(wrapper.text()).toContain(TEST_EMPTY_MESSAGE); - }); - }); - - describe('with no breakpoint', () => { - beforeEach(() => { - props.breakpoint = 0; - }); - - it('renders avatars', () => { - const items = createList(20); - factory({ propsData: { items } }); - - const links = wrapper.findAll(UserAvatarLink); - const linkProps = links.wrappers.map(x => x.props()); - - expect(linkProps).toEqual( - items.map(x => - jasmine.objectContaining({ - linkHref: x.web_url, - imgSrc: x.avatar_url, - imgAlt: x.name, - tooltipText: x.name, - imgSize: TEST_IMAGE_SIZE, - }), - ), - ); - }); - }); - - describe('with breakpoint and length equal to breakpoint', () => { - beforeEach(() => { - props.breakpoint = TEST_BREAKPOINT; - props.items = createList(TEST_BREAKPOINT); - }); - - it('renders all avatars if length is <= breakpoint', () => { - factory(); - - const links = wrapper.findAll(UserAvatarLink); - - expect(links.length).toEqual(props.items.length); - }); - - it('does not show button', () => { - factory(); - - expect(wrapper.find(GlButton).exists()).toBe(false); - }); - }); - - describe('with breakpoint and length greater than breakpoint', () => { - beforeEach(() => { - props.breakpoint = TEST_BREAKPOINT; - props.items = createList(TEST_BREAKPOINT + 1); - }); - - it('renders avatars up to breakpoint', () => { - factory(); - - const links = wrapper.findAll(UserAvatarLink); - - expect(links.length).toEqual(TEST_BREAKPOINT); - }); - - describe('with expand clicked', () => { - beforeEach(() => { - factory(); - clickButton(); - }); - - it('renders all avatars', () => { - const links = wrapper.findAll(UserAvatarLink); - - expect(links.length).toEqual(props.items.length); - }); - - it('with collapse clicked, it renders avatars up to breakpoint', () => { - clickButton(); - const links = wrapper.findAll(UserAvatarLink); - - expect(links.length).toEqual(TEST_BREAKPOINT); - }); - }); - }); -}); diff --git a/spec/lib/sentry/client/issue_spec.rb b/spec/lib/sentry/client/issue_spec.rb index 17548e2081d..20665c59a8d 100644 --- a/spec/lib/sentry/client/issue_spec.rb +++ b/spec/lib/sentry/client/issue_spec.rb @@ -74,6 +74,10 @@ describe Sentry::Client::Issue do it 'has a correct GitLab issue url' do expect(subject.gitlab_issue).to eq('https://gitlab.com/gitlab-org/gitlab/issues/1') end + + it 'has the correct tags' do + expect(subject.tags).to eq({ level: issue_sample_response['level'], logger: issue_sample_response['logger'] }) + end end end end diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb index c3db516f253..1346a43335e 100644 --- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb +++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb @@ -17,7 +17,8 @@ describe ChatMessage::WikiPageMessage do object_attributes: { title: 'Wiki page title', url: 'http://url.com', - content: 'Wiki page description' + content: 'Wiki page content', + message: 'Wiki page commit message' } } end @@ -57,10 +58,10 @@ describe ChatMessage::WikiPageMessage do args[:object_attributes][:action] = 'create' end - it 'returns the attachment for a new wiki page' do + it 'returns the commit message for a new wiki page' do expect(subject.attachments).to eq([ { - text: "Wiki page description", + text: "Wiki page commit message", color: color } ]) @@ -72,10 +73,10 @@ describe ChatMessage::WikiPageMessage do args[:object_attributes][:action] = 'update' end - it 'returns the attachment for an updated wiki page' do + it 'returns the commit message for an updated wiki page' do expect(subject.attachments).to eq([ { - text: "Wiki page description", + text: "Wiki page commit message", color: color } ]) @@ -119,8 +120,8 @@ describe ChatMessage::WikiPageMessage do args[:object_attributes][:action] = 'create' end - it 'returns the attachment for a new wiki page' do - expect(subject.attachments).to eq('Wiki page description') + it 'returns the commit message for a new wiki page' do + expect(subject.attachments).to eq('Wiki page commit message') end end @@ -129,8 +130,8 @@ describe ChatMessage::WikiPageMessage do args[:object_attributes][:action] = 'update' end - it 'returns the attachment for an updated wiki page' do - expect(subject.attachments).to eq('Wiki page description') + it 'returns the commit message for an updated wiki page' do + expect(subject.attachments).to eq('Wiki page commit message') end end end -- cgit v1.2.3