Project

General

Profile

Feature #42630 » 0002-Adds-tests-for-reaction-feature.patch

Katsuya HIDAKA, 2025-04-25 08:40

View differences:

test/fixtures/reactions.yml
1
---
2
reaction_001:
3
  id: 1
4
  reactable_type: Issue
5
  reactable_id: 1
6
  user_id: 1
7
reaction_002:
8
  id: 2
9
  reactable_type: Issue
10
  reactable_id: 1
11
  user_id: 2
12
reaction_003:
13
  id: 3
14
  reactable_type: Issue
15
  reactable_id: 1
16
  user_id: 3
17
reaction_004:
18
  id: 4
19
  reactable_type: Journal
20
  reactable_id: 1
21
  user_id: 2
22
reaction_005:
23
  id: 5
24
  reactable_type: Issue
25
  reactable_id: 6
26
  user_id: 2
27
reaction_006:
28
  id: 6
29
  reactable_type: Journal
30
  reactable_id: 4
31
  user_id: 2
32
reaction_007:
33
  id: 7
34
  reactable_type: News
35
  reactable_id: 1
36
  user_id: 1
37
reaction_008:
38
  id: 8
39
  reactable_type: Comment
40
  reactable_id: 1
41
  user_id: 2
42
reaction_009:
43
  id: 9
44
  reactable_type: Message
45
  reactable_id: 7
46
  user_id: 2
47
reaction_010:
48
  id: 10
49
  reactable_type: News
50
  reactable_id: 3
51
  user_id: 2
test/functional/issues_controller_test.rb
3331 3331
    assert_select 'span.badge.badge-private', text: 'Private'
3332 3332
  end
3333 3333

  
3334
  def test_show_should_display_reactions
3335
    @request.session[:user_id] = nil
3336

  
3337
    get :show, params: { id: 1 }
3338

  
3339
    assert_response :success
3340
    assert_select 'span[data-reaction-button-id=reaction_issue_1] span.reaction-button' do
3341
      assert_select 'span.icon-label', '3'
3342
    end
3343
    assert_select 'span[data-reaction-button-id=reaction_journal_1] span.reaction-button'
3344
    assert_select 'span[data-reaction-button-id=reaction_journal_2] span.reaction-button'
3345

  
3346
    # Should not display reactions when reactions feature is disabled.
3347
    Setting.reactions_enabled = false
3348
    get :show, params: { id: 1 }
3349

  
3350
    assert_response :success
3351
    assert_select 'span[data-reaction-button-id]', false
3352
  end
3353

  
3334 3354
  def test_show_should_not_display_edit_attachment_icon_for_user_without_edit_issue_permission_on_tracker
3335 3355
    role = Role.find(2)
3336 3356
    role.set_permission_trackers 'edit_issues', [2, 3]
test/functional/messages_controller_test.rb
123 123
    assert_select 'h3', {text: /Watchers \(\d*\)/, count: 0}
124 124
  end
125 125

  
126
  def test_show_should_display_reactions
127
    @request.session[:user_id] = 2
128

  
129
    get :show, params: { board_id: 1, id: 4 }
130

  
131
    assert_response :success
132
    assert_select 'span[data-reaction-button-id=reaction_message_4] a.reaction-button' do
133
      assert_select 'svg use[href*=thumb-up]'
134
    end
135
    assert_select 'span[data-reaction-button-id=reaction_message_5] a.reaction-button'
136
    assert_select 'span[data-reaction-button-id=reaction_message_6] a.reaction-button'
137

  
138
    # Should not display reactions when reactions feature is disabled.
139
    Setting.reactions_enabled = false
140
    get :show, params: { board_id: 1, id: 4 }
141

  
142
    assert_response :success
143
    assert_select 'span[data-reaction-button-id]', false
144
  end
145

  
126 146
  def test_get_new
127 147
    @request.session[:user_id] = 2
128 148
    get(:new, :params => {:board_id => 1})
test/functional/news_controller_test.rb
106 106
    assert_response :not_found
107 107
  end
108 108

  
109
  def test_show_should_display_reactions
110
    @request.session[:user_id] = 1
111

  
112
    get :show, params: { id: 1 }
113
    assert_response :success
114
    assert_select 'span[data-reaction-button-id=reaction_news_1] a.reaction-button.reacted'
115
    assert_select 'span[data-reaction-button-id=reaction_comment_1] a.reaction-button'
116

  
117
    # Should not display reactions when reactions feature is disabled.
118
    Setting.reactions_enabled = false
119
    get :show, params: { id: 1 }
120
    assert_response :success
121
    assert_select 'span[data-reaction-button-id]', false
122
  end
123

  
109 124
  def test_get_new_with_project_id
110 125
    @request.session[:user_id] = 2
111 126
    get(:new, :params => {:project_id => 1})
test/functional/reactions_controller_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../test_helper'
21

  
22
class ReactionsControllerTest < Redmine::ControllerTest
23
  def setup
24
    Setting.reactions_enabled = '1'
25
    # jsmith
26
    @request.session[:user_id] = users(:users_002).id
27
  end
28

  
29
  test 'create for issue' do
30
    issue = issues(:issues_002)
31

  
32
    assert_difference(
33
      ->{ Reaction.count } => 1,
34
      ->{ issue.reactions.by(users(:users_002)).count } => 1
35
    ) do
36
      post :create, params: {
37
        object_type: 'Issue',
38
        object_id: issue.id
39
      }, xhr: true
40
    end
41

  
42
    assert_response :success
43
  end
44

  
45
  test 'create for journal' do
46
    journal = journals(:journals_005)
47

  
48
    assert_difference(
49
      ->{ Reaction.count } => 1,
50
      ->{ journal.reactions.by(users(:users_002)).count } => 1
51
    ) do
52
      post :create, params: {
53
        object_type: 'Journal',
54
        object_id: journal.id
55
      }, xhr: true
56
    end
57

  
58
    assert_response :success
59
  end
60

  
61
  test 'create for news' do
62
    news = news(:news_002)
63

  
64
    assert_difference(
65
      ->{ Reaction.count } => 1,
66
      ->{ news.reactions.by(users(:users_002)).count } => 1
67
    ) do
68
      post :create, params: {
69
        object_type: 'News',
70
        object_id: news.id
71
      }, xhr: true
72
    end
73

  
74
    assert_response :success
75
  end
76

  
77
  test 'create reaction for comment' do
78
    comment = comments(:comments_002)
79

  
80
    assert_difference(
81
      ->{ Reaction.count } => 1,
82
      ->{ comment.reactions.by(users(:users_002)).count } => 1
83
    ) do
84
      post :create, params: {
85
        object_type: 'Comment',
86
        object_id: comment.id
87
      }, xhr: true
88
    end
89

  
90
    assert_response :success
91
  end
92

  
93
  test 'create for message' do
94
    message = messages(:messages_001)
95

  
96
    assert_difference(
97
      ->{ Reaction.count } => 1,
98
      ->{ message.reactions.by(users(:users_002)).count } => 1
99
    ) do
100
      post :create, params: {
101
        object_type: 'Message',
102
        object_id: message.id
103
      }, xhr: true
104
    end
105

  
106
    assert_response :success
107
  end
108

  
109
  test 'destroy for issue' do
110
    reaction = reactions(:reaction_005)
111

  
112
    assert_difference 'Reaction.count', -1 do
113
      delete :destroy, params: {
114
        id: reaction.id,
115
        # Issue (id=6)
116
        object_type: reaction.reactable_type,
117
        object_id: reaction.reactable_id
118
      }, xhr: true
119
    end
120

  
121
    assert_response :success
122
    assert_not Reaction.exists?(reaction.id)
123
  end
124

  
125
  test 'destroy for journal' do
126
    reaction = reactions(:reaction_006)
127

  
128
    assert_difference 'Reaction.count', -1 do
129
      delete :destroy, params: {
130
        id: reaction.id,
131
        object_type: reaction.reactable_type,
132
        object_id: reaction.reactable_id
133
      }, xhr: true
134
    end
135

  
136
    assert_response :success
137
    assert_not Reaction.exists?(reaction.id)
138
  end
139

  
140
  test 'destroy for news' do
141
    # For News(id=3)
142
    reaction = reactions(:reaction_010)
143

  
144
    assert_difference 'Reaction.count', -1 do
145
      delete :destroy, params: {
146
        id: reaction.id,
147
        object_type: reaction.reactable_type,
148
        object_id: reaction.reactable_id
149
      }, xhr: true
150
    end
151

  
152
    assert_response :success
153
    assert_not Reaction.exists?(reaction.id)
154
  end
155

  
156
  test 'destroy for comment' do
157
    # For Comment(id=1)
158
    reaction = reactions(:reaction_008)
159

  
160
    assert_difference 'Reaction.count', -1 do
161
      delete :destroy, params: {
162
        id: reaction.id,
163
        object_type: reaction.reactable_type,
164
        object_id: reaction.reactable_id
165
      }, xhr: true
166
    end
167

  
168
    assert_response :success
169
    assert_not Reaction.exists?(reaction.id)
170
  end
171

  
172
  test 'destroy for message' do
173
    reaction = reactions(:reaction_009)
174

  
175
    assert_difference 'Reaction.count', -1 do
176
      delete :destroy, params: {
177
        id: reaction.id,
178
        object_type: reaction.reactable_type,
179
        object_id: reaction.reactable_id
180
      }, xhr: true
181
    end
182

  
183
    assert_response :success
184
    assert_not Reaction.exists?(reaction.id)
185
  end
186

  
187
  test 'create should respond with 403 when feature is disabled' do
188
    Setting.reactions_enabled = '0'
189
    # admin
190
    @request.session[:user_id] = users(:users_001).id
191

  
192
    assert_no_difference 'Reaction.count' do
193
      post :create, params: {
194
        object_type: 'Issue',
195
        object_id: issues(:issues_002).id
196
      }, xhr: true
197
    end
198
    assert_response :forbidden
199
  end
200

  
201
  test 'destroy should respond with 403 when feature is disabled' do
202
    Setting.reactions_enabled = '0'
203
    # admin
204
    @request.session[:user_id] = users(:users_001).id
205

  
206
    reaction = reactions(:reaction_001)
207
    assert_no_difference 'Reaction.count' do
208
      delete :destroy, params: {
209
        id: reaction.id,
210
        object_type: reaction.reactable_type,
211
        object_id: reaction.reactable_id
212
      }, xhr: true
213
    end
214
    assert_response :forbidden
215
  end
216

  
217
  test 'create by anonymous user should respond with 403' do
218
    @request.session[:user_id] = nil
219

  
220
    assert_no_difference 'Reaction.count' do
221
      post :create, params: {
222
        object_type: 'Issue',
223
        # Issue(id=1) is an issue in a public project
224
        object_id: issues(:issues_001).id
225
      }, xhr: true
226
    end
227

  
228
    assert_response :unauthorized
229
  end
230

  
231
  test 'destroy by anonymous user should respond with 401' do
232
    @request.session[:user_id] = nil
233

  
234
    reaction = reactions(:reaction_002)
235
    assert_no_difference 'Reaction.count' do
236
      delete :destroy, params: {
237
        id: reaction.id,
238
        object_type: reaction.reactable_type,
239
        object_id: reaction.reactable_id
240
      }, xhr: true
241
    end
242

  
243
    assert_response :unauthorized
244
  end
245

  
246
  test 'create when reaction already exists should not create a new reaction and succeed' do
247
    assert_no_difference 'Reaction.count' do
248
      post :create, params: {
249
        object_type: 'Comment',
250
        # user(jsmith) has already reacted to Comment(id=1)
251
        object_id: comments(:comments_001).id
252
      }, xhr: true
253
    end
254

  
255
    assert_response :success
256
  end
257

  
258
  test 'destroy another user reaction should not destroy the reaction and succeed' do
259
    # admin user's reaction
260
    reaction = reactions(:reaction_001)
261

  
262
    assert_no_difference 'Reaction.count' do
263
      delete :destroy, params: {
264
        id: reaction.id,
265
        object_type: reaction.reactable_type,
266
        object_id: reaction.reactable_id
267
      }, xhr: true
268
    end
269

  
270
    assert_response :success
271
  end
272

  
273
  test 'destroy nonexistent reaction' do
274
    # For Journal(id=4)
275
    reaction = reactions(:reaction_006)
276
    reaction.destroy!
277

  
278
    assert_not Reaction.exists?(reaction.id)
279

  
280
    assert_no_difference 'Reaction.count' do
281
      delete :destroy, params: {
282
        id: reaction.id,
283
        object_type: reaction.reactable_type,
284
        object_id: reaction.reactable_id
285
      }, xhr: true
286
    end
287

  
288
    assert_response :success
289
  end
290

  
291
  test 'create with invalid object type should respond with 403' do
292
    # admin
293
    @request.session[:user_id] = users(:users_001).id
294

  
295
    post :create, params: {
296
      object_type: 'InvalidType',
297
      object_id: 1
298
    }, xhr: true
299

  
300
    assert_response :forbidden
301
  end
302

  
303
  test 'create without permission to view should respond with 403' do
304
    # dlopper
305
    @request.session[:user_id] = users(:users_003).id
306

  
307
    assert_no_difference 'Reaction.count' do
308
      post :create, params: {
309
        object_type: 'Issue',
310
        # dlopper is not a member of the project where the issue (id=4) belongs.
311
        object_id: issues(:issues_004).id
312
      }, xhr: true
313
    end
314

  
315
    assert_response :forbidden
316
  end
317

  
318
  test 'destroy without permission to view should respond with 403' do
319
    # dlopper
320
    @request.session[:user_id] = users(:users_003).id
321

  
322
    # For Issue(id=6)
323
    reaction = reactions(:reaction_005)
324

  
325
    assert_no_difference 'Reaction.count' do
326
      delete :destroy, params: {
327
        id: reaction.id,
328
        object_type: reaction.reactable_type,
329
        object_id: reaction.reactable_id
330
      }, xhr: true
331
    end
332

  
333
    assert_response :forbidden
334
  end
335
end
test/helpers/reactions_helper_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../test_helper'
21

  
22
class ReactionsHelperTest < ActionView::TestCase
23
  include ReactionsHelper
24

  
25
  setup do
26
    User.current = users(:users_002)
27
    Setting.reactions_enabled = '1'
28
  end
29

  
30
  test 'reaction_id_for generates a DOM id' do
31
    assert_equal "reaction_issue_1", reaction_id_for(issues(:issues_001))
32
  end
33

  
34
  test 'reaction_button returns nil when feature is disabled' do
35
    Setting.reactions_enabled = '0'
36

  
37
    assert_nil reaction_button(issues(:issues_004))
38
  end
39

  
40
  test 'reaction_button returns nil when object not visible' do
41
    User.current = users(:users_003)
42

  
43
    assert_nil reaction_button(issues(:issues_004))
44
  end
45

  
46
  test 'reaction_button for anonymous users shows static icon' do
47
    User.current = nil
48

  
49
    result = reaction_button(journals(:journals_001))
50

  
51
    assert_select_in result, 'span.reaction-button[title=?]', 'John Smith'
52
    assert_select_in result, 'a.reaction-button', false
53
  end
54

  
55
  test 'reaction_button includes no tooltip when the object has no reactions' do
56
    issue = issues(:issues_002) # Issue without reactions
57
    result = reaction_button(issue)
58

  
59
    assert_select_in result, 'span.reaction-button[title]', false
60
  end
61

  
62
  test 'reaction_button includes tooltip with all usernames when reactions are 10 or fewer' do
63
    issue = issues(:issues_002)
64

  
65
    reactions = build_reactions(10)
66
    issue.reactions += reactions
67

  
68
    result = with_locale 'en' do
69
      reaction_button(issue)
70
    end
71

  
72
    # The tooltip should display usernames in order of newest reactions.
73
    expected_tooltip = 'Bob9 Doe, Bob8 Doe, Bob7 Doe, Bob6 Doe, Bob5 Doe, ' \
74
                       'Bob4 Doe, Bob3 Doe, Bob2 Doe, Bob1 Doe, and Bob0 Doe'
75

  
76
    assert_select_in result, 'a.reaction-button[title=?]', expected_tooltip
77
  end
78

  
79
  test 'reaction_button includes tooltip with 10 usernames and others count when reactions exceed 10' do
80
    issue = issues(:issues_002)
81

  
82
    reactions = build_reactions(11)
83
    issue.reactions += reactions
84

  
85
    result = with_locale 'en' do
86
      reaction_button(issue)
87
    end
88

  
89
    expected_tooltip = 'Bob10 Doe, Bob9 Doe, Bob8 Doe, Bob7 Doe, Bob6 Doe, ' \
90
                       'Bob5 Doe, Bob4 Doe, Bob3 Doe, Bob2 Doe, Bob1 Doe, and 1 other'
91

  
92
    assert_select_in result, 'a.reaction-button[title=?]', expected_tooltip
93
  end
94

  
95
  test 'reaction_button for reacted object' do
96
    issue = issues(:issues_001)
97
    reaction = issue.reactions.find_by(user: User.current)
98

  
99
    result = with_locale('en') do
100
      reaction_button(issue, reaction)
101
    end
102
    tooltip = 'Dave Lopper, John Smith, and Redmine Admin'
103

  
104
    assert_select_in result, 'span[data-reaction-button-id=?]', 'reaction_issue_1' do
105
      href = reaction_path(reaction, object_type: 'Issue', object_id: 1)
106

  
107
      assert_select 'a.icon.reaction-button.reacted[href=?]', href do
108
        assert_select 'use[href*=?]', 'thumb-up-filled'
109
        assert_select 'span.icon-label', '3'
110
      end
111

  
112
      assert_select 'span.reaction-button', false
113
    end
114
  end
115

  
116
  test 'reaction_button for non-reacted object' do
117
    User.current = users(:users_004)
118

  
119
    issue = issues(:issues_001)
120
    reaction = issue.reactions.find_by(user: User.current)
121

  
122
    result = with_locale('en') do
123
      reaction_button(issue, reaction)
124
    end
125
    tooltip = 'Dave Lopper, John Smith, and Redmine Admin'
126

  
127
    assert_select_in result, 'span[data-reaction-button-id=?]', 'reaction_issue_1' do
128
      href = reactions_path(object_type: 'Issue', object_id: 1)
129

  
130
      assert_select 'a.icon.reaction-button[href=?]', href do
131
        assert_select 'use[href*=?]', 'thumb-up'
132
        assert_select 'span.icon-label', '3'
133
      end
134

  
135
      assert_select 'span.reaction-button', false
136
    end
137
  end
138

  
139
  private
140

  
141
  def build_reactions(count)
142
    Array.new(count) do |i|
143
      Reaction.new(user: User.generate!(firstname: "Bob#{i}"))
144
    end
145
  end
146
end
test/system/reactions_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../application_system_test_case'
21

  
22
class ReactionsSystemTest < ApplicationSystemTestCase
23
  def test_react_to_issue
24
    log_user('jsmith', 'jsmith')
25

  
26
    issue = issues(:issues_002)
27

  
28
    with_settings(reactions_enabled: '1') do
29
      visit '/issues/2'
30
      reaction_button = find("div.issue.details [data-reaction-button-id=\"reaction_issue_#{issue.id}\"]")
31
      assert_reaction_add_and_remove(reaction_button, issue)
32
    end
33
  end
34

  
35
  def test_react_to_journal
36
    log_user('jsmith', 'jsmith')
37

  
38
    journal = journals(:journals_002)
39

  
40
    with_settings(reactions_enabled: '1') do
41
      visit '/issues/1'
42
      reaction_button = find("[data-reaction-button-id=\"reaction_journal_#{journal.id}\"]")
43
      assert_reaction_add_and_remove(reaction_button, journal.reload)
44
    end
45
  end
46

  
47
  def test_react_to_forum_reply
48
    log_user('jsmith', 'jsmith')
49

  
50
    reply_message = messages(:messages_002) # reply to message_001
51

  
52
    with_settings(reactions_enabled: '1') do
53
      visit 'boards/1/topics/1'
54
      reaction_button = find("[data-reaction-button-id=\"reaction_message_#{reply_message.id}\"]")
55
      assert_reaction_add_and_remove(reaction_button, reply_message)
56
    end
57
  end
58

  
59
  def test_react_to_forum_message
60
    log_user('jsmith', 'jsmith')
61

  
62
    message = messages(:messages_001)
63

  
64
    with_settings(reactions_enabled: '1') do
65
      visit 'boards/1/topics/1'
66
      reaction_button = find("[data-reaction-button-id=\"reaction_message_#{message.id}\"]")
67
      assert_reaction_add_and_remove(reaction_button, message)
68
    end
69
  end
70

  
71
  def test_react_to_news
72
    log_user('jsmith', 'jsmith')
73

  
74
    with_settings(reactions_enabled: '1') do
75
      visit '/news/2'
76
      reaction_button = find("[data-reaction-button-id=\"reaction_news_2\"]")
77
      assert_reaction_add_and_remove(reaction_button, news(:news_002))
78
    end
79
  end
80

  
81
  def test_react_to_comment
82
    log_user('jsmith', 'jsmith')
83

  
84
    comment = comments(:comments_002)
85

  
86
    with_settings(reactions_enabled: '1') do
87
      visit '/news/1'
88
      reaction_button = find("[data-reaction-button-id=\"reaction_comment_#{comment.id}\"]")
89
      assert_reaction_add_and_remove(reaction_button, comment)
90
    end
91
  end
92

  
93
  def test_reactions_disabled
94
    log_user('jsmith', 'jsmith')
95

  
96
    with_settings(reactions_enabled: '0') do
97
      visit '/issues/1'
98
      assert_no_selector('[data-reaction-button-id="reaction_issue_1"]')
99
    end
100
  end
101

  
102
  def test_reaction_button_is_visible_but_not_clickable_for_not_logged_in_user
103
    with_settings(reactions_enabled: '1') do
104
      visit '/issues/1'
105

  
106
      # visible
107
      reaction_button = find('div.issue.details [data-reaction-button-id="reaction_issue_1"]')
108
      within(reaction_button) { assert_selector('span.reaction-button') }
109
      assert_equal "3", reaction_button.text
110

  
111
      # not clickable
112
      within(reaction_button) { assert_no_selector('a.reaction-button') }
113
    end
114
  end
115

  
116
  private
117

  
118
  def assert_reaction_add_and_remove(reaction_button, expected_subject)
119
    # Add a reaction
120
    within(reaction_button) { find('a.reaction-button').click }
121
    find('body').hover # Hide tooltip
122
    within(reaction_button) { assert_selector('a.reaction-button.reacted[title="John Smith"]') }
123
    assert_equal "1", reaction_button.text
124
    assert_equal 1, expected_subject.reactions.count
125

  
126
    # Remove the reaction
127
    within(reaction_button) { find('a.reacted').click }
128
    within(reaction_button) { assert_selector('a.reaction-button:not(.reacted)') }
129
    assert_equal "0", reaction_button.text
130
    assert_equal 0, expected_subject.reactions.count
131
  end
132
end
test/unit/lib/redmine/reaction_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../../../test_helper'
21

  
22
class Redmine::ReactionTest < ActiveSupport::TestCase
23
  setup do
24
    @user = users(:users_002)
25
    @issue = issues(:issues_007)
26
    Setting.reactions_enabled = '1'
27
  end
28

  
29
  test 'reaction_by returns reaction for specific user' do
30
    reaction = Reaction.create(reactable: @issue, user: @user)
31

  
32
    assert_equal reaction, @issue.reaction_by(@user)
33
    assert_nil @issue.reaction_by(users(:users_003)) # Another user
34
  end
35

  
36
  test 'reaction_by finds reaction when reactions are preloaded' do
37
    reaction = Reaction.create(reactable: @issue, user: @user)
38

  
39
    issue_with_reactions = Issue.preload(:reactions).find(@issue.id)
40

  
41
    assert_no_queries do
42
      assert_equal reaction.id, issue_with_reactions.reaction_by(@user).id
43
    end
44
  end
45

  
46
  test 'load_with_reactions returns an array and preloads reaction_user_names' do
47
    issues = Issue.where(id: [1, 6]).order(:id).load_with_reactions
48

  
49
    assert_equal [issues(:issues_001), issues(:issues_006)], issues
50

  
51
    assert_no_queries do
52
      assert_equal ['Dave Lopper', 'John Smith', 'Redmine Admin'], issues.first.reaction_user_names
53
      assert_equal ['John Smith'], issues.second.reaction_user_names
54
    end
55
  end
56

  
57
  test 'load_with_reactions returns an array and does not preload reaction_user_names' do
58
    Setting.reactions_enabled = '0'
59

  
60
    journals = Journal.where(id: 1).load_with_reactions
61

  
62
    assert_equal [journals(:journals_001)], journals
63
    assert_nil journals.first.instance_variable_get(:@reaction_user_names)
64
  end
65

  
66
  test 'reaction_user_names returns an array of user names' do
67
    assert_equal ['John Smith'], comments(:comments_001).reaction_user_names
68

  
69
    # When user names are preloaded by load_with_reactions
70
    comment = Comment.where(id: [1]).load_with_reactions.first
71
    assert_no_queries do
72
      assert_equal ['John Smith'], comment.reaction_user_names
73
    end
74
  end
75

  
76
  test 'reaction_users returns users ordered by their newest reactions' do
77
    Reaction.create(reactable: @issue, user: users(:users_001))
78
    Reaction.create(reactable: @issue, user: users(:users_002))
79
    Reaction.create(reactable: @issue, user: users(:users_003))
80

  
81
    assert_equal [
82
      users(:users_003),
83
      users(:users_002),
84
      users(:users_001)
85
    ], @issue.reaction_users
86
  end
87

  
88
  test 'destroy should delete associated reactions' do
89
    @issue.reactions.create!(
90
      [
91
        {user: users(:users_001)},
92
        {user: users(:users_002)}
93
      ]
94
    )
95
    assert_difference 'Reaction.count', -2 do
96
      @issue.destroy
97
    end
98
  end
99
end
test/unit/reaction_test.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative '../test_helper'
21

  
22
class ReactionTest < ActiveSupport::TestCase
23
  test 'validates :inclusion of reactable_type' do
24
    %w(Issue Journal News Comment Message).each do |type|
25
      reaction = Reaction.new(reactable_type: type, user: User.new)
26
      assert reaction.valid?
27
    end
28

  
29
    assert_not Reaction.new(reactable_type: 'InvalidType', user: User.new).valid?
30
  end
31

  
32
  test 'scope: by' do
33
    user2_reactions = issues(:issues_001).reactions.by(users(:users_002))
34

  
35
    assert_equal [reactions(:reaction_002)], user2_reactions
36
  end
37

  
38
  test 'users_map_for_reactables returns correct mapping for given reactable_type and reactable_ids' do
39
    issue1 = issues(:issues_001)
40
    issue6 = issues(:issues_006)
41

  
42
    result = Reaction.users_map_for_reactables('Issue', [issue1.id, issue6.id])
43

  
44
    expected_result = {
45
      issue1.id => ['Dave Lopper', 'John Smith', 'Redmine Admin'],
46
      issue6.id => ['John Smith']
47
    }
48

  
49
    assert_equal expected_result, result
50
  end
51

  
52
  test 'users_map_for_reactables returns empty hash when no reactions exist' do
53
    result = Reaction.users_map_for_reactables('Issue', [3, 4, 5])
54

  
55
    assert_equal({}, result)
56
  end
57

  
58
  test "should prevent duplicate reactions with unique constraint under concurrent creation" do
59
    user = users(:users_001)
60
    issue = issues(:issues_004)
61

  
62
    threads = []
63
    results = []
64

  
65
    # Ensure both threads start at the same time
66
    barrier = Concurrent::CyclicBarrier.new(2)
67

  
68
    # Create two threads to simulate concurrent creation
69
    2.times do
70
      threads << Thread.new do
71
        barrier.wait # Wait for both threads to be ready
72
        begin
73
          reaction = Reaction.create(
74
            reactable: issue,
75
            user: user
76
          )
77
          results << reaction.persisted?
78
        rescue ActiveRecord::RecordNotUnique
79
          results << false
80
        end
81
      end
82
    end
83

  
84
    # Wait for both threads to finish
85
    threads.each(&:join)
86

  
87
    # Ensure only one reaction was created
88
    assert_equal 1, Reaction.where(reactable: issue, user: user).count
89
    assert_includes results, true
90
    assert_equal 1, results.count(true)
91
  end
92
end
test/unit/user_test.rb
1376 1376
      User.prune(7)
1377 1377
    end
1378 1378
  end
1379

  
1380
  def test_destroy_should_delete_associated_reactions
1381
    users(:users_004).reactions.create!(
1382
      [
1383
        {reactable: issues(:issues_001)},
1384
        {reactable: issues(:issues_002)}
1385
      ]
1386
    )
1387
    assert_difference 'Reaction.count', -2 do
1388
      users(:users_004).destroy
1389
    end
1390
  end
1379 1391
end
(8-8/9)