Просмотр исходного кода

Fixes #5491 - Deadlocks on updating taskbars.

Co-authored-by: Florian Liebe <fl@zammad.com>
Rolf Schmidt 3 недель назад
Родитель
Сommit
22a0b20a54

+ 15 - 0
app/jobs/taskbar_update_related_tasks_job.rb

@@ -0,0 +1,15 @@
+# Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
+
+class TaskbarUpdateRelatedTasksJob < ApplicationJob
+  def perform(task_ids)
+    Taskbar.where(id: task_ids).each do |taskbar|
+      taskbar.with_lock do
+        taskbar.update!(
+          preferences:       taskbar.preferences.merge(tasks: taskbar.collect_related_tasks),
+          local_update:      true,
+          skip_item_trigger: true,
+        )
+      end
+    end
+  end
+end

+ 19 - 18
app/models/taskbar.rb

@@ -29,7 +29,8 @@ class Taskbar < ApplicationModel
   before_update   :update_last_contact, :set_user, :update_preferences_infos
   before_update   :update_last_contact, :set_user, :update_preferences_infos
 
 
   after_update    :notify_clients
   after_update    :notify_clients
-  after_destroy   :update_preferences_infos, :notify_clients
+  after_destroy :update_preferences_infos, :notify_clients
+  after_commit :update_related_taskbars
 
 
   association_attributes_ignored :user
   association_attributes_ignored :user
 
 
@@ -96,6 +97,14 @@ class Taskbar < ApplicationModel
     save!
     save!
   end
   end
 
 
+  def collect_related_tasks
+    related_taskbars.map(&:preferences_task_info)
+      .tap { |arr| arr.push(preferences_task_info) if !destroyed? }
+      .each_with_object({}) { |elem, memo| reduce_related_tasks(elem, memo) }
+      .values
+      .sort_by { |elem| elem[:id] || Float::MAX } # sort by IDs to pass old tests
+  end
+
   private
   private
 
 
   def update_last_contact
   def update_last_contact
@@ -125,22 +134,17 @@ class Taskbar < ApplicationModel
   def update_preferences_infos
   def update_preferences_infos
     return if key == 'Search'
     return if key == 'Search'
     return if local_update
     return if local_update
+    return if changed_only_prio?
 
 
     preferences = self.preferences || {}
     preferences = self.preferences || {}
     preferences[:tasks] = collect_related_tasks
     preferences[:tasks] = collect_related_tasks
 
 
-    update_related_taskbars(preferences)
-
     # remember preferences for current taskbar
     # remember preferences for current taskbar
     self.preferences = preferences if !destroyed?
     self.preferences = preferences if !destroyed?
   end
   end
 
 
-  def collect_related_tasks
-    related_taskbars.map(&:preferences_task_info)
-      .tap { |arr| arr.push(preferences_task_info) if !destroyed? }
-      .each_with_object({}) { |elem, memo| reduce_related_tasks(elem, memo) }
-      .values
-      .sort_by { |elem| elem[:id] || Float::MAX } # sort by IDs to pass old tests
+  def changed_only_prio?
+    changed_attribute_names_to_save.to_set == Set.new(%w[updated_at prio])
   end
   end
 
 
   def reduce_related_tasks(elem, memo)
   def reduce_related_tasks(elem, memo)
@@ -154,15 +158,12 @@ class Taskbar < ApplicationModel
     memo[key] = elem
     memo[key] = elem
   end
   end
 
 
-  def update_related_taskbars(preferences)
-    related_taskbars.each do |taskbar|
-      taskbar.with_lock do
-        taskbar.preferences = preferences
-        taskbar.local_update = true
-        taskbar.skip_item_trigger = true
-        taskbar.save!
-      end
-    end
+  def update_related_taskbars
+    return if key == 'Search'
+    return if local_update
+    return if changed_only_prio?
+
+    TaskbarUpdateRelatedTasksJob.perform_later(related_taskbars.map(&:id))
   end
   end
 
 
   def notify_clients
   def notify_clients

+ 3 - 1
spec/graphql/gql/subscriptions/ticket_live_user_updates_spec.rb

@@ -2,7 +2,7 @@
 
 
 require 'rails_helper'
 require 'rails_helper'
 
 
-RSpec.describe Gql::Subscriptions::TicketLiveUserUpdates, :aggregate_failures, authenticated_as: :agent, type: :graphql do
+RSpec.describe Gql::Subscriptions::TicketLiveUserUpdates, :aggregate_failures, authenticated_as: :agent, performs_jobs: true, type: :graphql do
   let(:agent)                         { create(:agent) }
   let(:agent)                         { create(:agent) }
   let(:customer)                      { create(:customer) }
   let(:customer)                      { create(:customer) }
   let(:ticket)                        { create(:ticket) }
   let(:ticket)                        { create(:ticket) }
@@ -33,6 +33,7 @@ RSpec.describe Gql::Subscriptions::TicketLiveUserUpdates, :aggregate_failures, a
 
 
   before do
   before do
     live_user_entry && live_user_entry_customer
     live_user_entry && live_user_entry_customer
+    perform_enqueued_jobs
 
 
     gql.execute(subscription, variables: variables, context: { channel: mock_channel })
     gql.execute(subscription, variables: variables, context: { channel: mock_channel })
   end
   end
@@ -42,6 +43,7 @@ RSpec.describe Gql::Subscriptions::TicketLiveUserUpdates, :aggregate_failures, a
     # We need to work around this, otherwise this test would fail.
     # We need to work around this, otherwise this test would fail.
     UserInfo.current_user_id = agent_id
     UserInfo.current_user_id = agent_id
     taskbar_item.update!(state: state)
     taskbar_item.update!(state: state)
+    perform_enqueued_jobs
     UserInfo.current_user_id = agent.id
     UserInfo.current_user_id = agent.id
   end
   end
 
 

+ 6 - 1
spec/models/taskbar/triggers_subscriptions_spec.rb

@@ -2,7 +2,7 @@
 
 
 require 'rails_helper'
 require 'rails_helper'
 
 
-RSpec.describe Taskbar::TriggersSubscriptions, :aggregate_failures do
+RSpec.describe Taskbar::TriggersSubscriptions, :aggregate_failures, performs_jobs: true do
   let(:taskbar)         { create(:taskbar, user: create(:user)) }
   let(:taskbar)         { create(:taskbar, user: create(:user)) }
   let(:related_taskbar) { create(:taskbar, key: taskbar.key, user: create(:user)) }
   let(:related_taskbar) { create(:taskbar, key: taskbar.key, user: create(:user)) }
 
 
@@ -34,6 +34,7 @@ RSpec.describe Taskbar::TriggersSubscriptions, :aggregate_failures do
     it 'triggers correctly' do
     it 'triggers correctly' do
       taskbar.prio += 1
       taskbar.prio += 1
       taskbar.save!
       taskbar.save!
+      perform_enqueued_jobs
       expect(gqs::TicketLiveUserUpdates).to have_received(:trigger).exactly(2)
       expect(gqs::TicketLiveUserUpdates).to have_received(:trigger).exactly(2)
       expect(gqs_uc::TaskbarItemUpdates).not_to have_received(:trigger_after_update)
       expect(gqs_uc::TaskbarItemUpdates).not_to have_received(:trigger_after_update)
       expect(gqs_uc::TaskbarItemStateUpdates).not_to have_received(:trigger)
       expect(gqs_uc::TaskbarItemStateUpdates).not_to have_received(:trigger)
@@ -43,6 +44,7 @@ RSpec.describe Taskbar::TriggersSubscriptions, :aggregate_failures do
   context 'when updating last_contact_at' do
   context 'when updating last_contact_at' do
     it 'triggers correctly' do
     it 'triggers correctly' do
       taskbar.touch_last_contact!
       taskbar.touch_last_contact!
+      perform_enqueued_jobs
       expect(gqs::TicketLiveUserUpdates).to have_received(:trigger).exactly(1) # only for related_taskbar
       expect(gqs::TicketLiveUserUpdates).to have_received(:trigger).exactly(1) # only for related_taskbar
       expect(gqs_uc::TaskbarItemUpdates).not_to have_received(:trigger_after_update)
       expect(gqs_uc::TaskbarItemUpdates).not_to have_received(:trigger_after_update)
       expect(gqs_uc::TaskbarItemStateUpdates).not_to have_received(:trigger)
       expect(gqs_uc::TaskbarItemStateUpdates).not_to have_received(:trigger)
@@ -54,6 +56,7 @@ RSpec.describe Taskbar::TriggersSubscriptions, :aggregate_failures do
       it 'triggers correctly' do
       it 'triggers correctly' do
         taskbar.state = { 'body' => 'test' }
         taskbar.state = { 'body' => 'test' }
         taskbar.save!
         taskbar.save!
+        perform_enqueued_jobs
         expect(gqs::TicketLiveUserUpdates).to have_received(:trigger).exactly(2)
         expect(gqs::TicketLiveUserUpdates).to have_received(:trigger).exactly(2)
         expect(gqs_uc::TaskbarItemUpdates).to have_received(:trigger_after_update).once # only for taskbar
         expect(gqs_uc::TaskbarItemUpdates).to have_received(:trigger_after_update).once # only for taskbar
         expect(gqs_uc::TaskbarItemStateUpdates).to have_received(:trigger).once
         expect(gqs_uc::TaskbarItemStateUpdates).to have_received(:trigger).once
@@ -66,6 +69,7 @@ RSpec.describe Taskbar::TriggersSubscriptions, :aggregate_failures do
       it 'triggers correctly' do
       it 'triggers correctly' do
         taskbar.state = { 'body' => 'test' }
         taskbar.state = { 'body' => 'test' }
         taskbar.save!
         taskbar.save!
+        perform_enqueued_jobs
         expect(gqs::TicketLiveUserUpdates).to have_received(:trigger).exactly(2)
         expect(gqs::TicketLiveUserUpdates).to have_received(:trigger).exactly(2)
         expect(gqs_uc::TaskbarItemUpdates).to have_received(:trigger_after_update).once # only for taskbar
         expect(gqs_uc::TaskbarItemUpdates).to have_received(:trigger_after_update).once # only for taskbar
         expect(gqs_uc::TaskbarItemStateUpdates).not_to have_received(:trigger)
         expect(gqs_uc::TaskbarItemStateUpdates).not_to have_received(:trigger)
@@ -76,6 +80,7 @@ RSpec.describe Taskbar::TriggersSubscriptions, :aggregate_failures do
   context 'when deleting the record' do
   context 'when deleting the record' do
     it 'triggers correctly' do
     it 'triggers correctly' do
       taskbar.destroy!
       taskbar.destroy!
+      perform_enqueued_jobs
       expect(gqs::TicketLiveUserUpdates).to have_received(:trigger).once # only for related_taskbar
       expect(gqs::TicketLiveUserUpdates).to have_received(:trigger).once # only for related_taskbar
       expect(gqs_uc::TaskbarItemUpdates).to have_received(:trigger_after_destroy)
       expect(gqs_uc::TaskbarItemUpdates).to have_received(:trigger_after_destroy)
       expect(gqs_uc::TaskbarItemStateUpdates).not_to have_received(:trigger)
       expect(gqs_uc::TaskbarItemStateUpdates).not_to have_received(:trigger)

+ 24 - 4
spec/models/taskbar_spec.rb

@@ -3,7 +3,7 @@
 require 'rails_helper'
 require 'rails_helper'
 require 'models/taskbar/has_attachments_examples'
 require 'models/taskbar/has_attachments_examples'
 
 
-RSpec.describe Taskbar, type: :model do
+RSpec.describe Taskbar, performs_jobs: true, type: :model do
   it_behaves_like 'Taskbar::HasAttachments'
   it_behaves_like 'Taskbar::HasAttachments'
 
 
   context 'item' do
   context 'item' do
@@ -146,6 +146,8 @@ RSpec.describe Taskbar, type: :model do
         user_id:  1,
         user_id:  1,
       )
       )
 
 
+      perform_enqueued_jobs
+
       taskbar1.reload
       taskbar1.reload
       expect(taskbar1.preferences[:tasks].count).to eq(2)
       expect(taskbar1.preferences[:tasks].count).to eq(2)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
@@ -172,6 +174,8 @@ RSpec.describe Taskbar, type: :model do
         user_id:  1,
         user_id:  1,
       )
       )
 
 
+      perform_enqueued_jobs
+
       taskbar1.reload
       taskbar1.reload
       expect(taskbar1.preferences[:tasks].count).to eq(2)
       expect(taskbar1.preferences[:tasks].count).to eq(2)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
@@ -206,6 +210,8 @@ RSpec.describe Taskbar, type: :model do
         user_id:  1,
         user_id:  1,
       )
       )
 
 
+      perform_enqueued_jobs
+
       taskbar1.reload
       taskbar1.reload
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
@@ -242,6 +248,8 @@ RSpec.describe Taskbar, type: :model do
       taskbar2.state = { article: {}, ticket: {} }
       taskbar2.state = { article: {}, ticket: {} }
       taskbar2.save!
       taskbar2.save!
 
 
+      perform_enqueued_jobs
+
       taskbar1.reload
       taskbar1.reload
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
@@ -278,6 +286,8 @@ RSpec.describe Taskbar, type: :model do
       taskbar2.state = { article: { body: 'some body' }, ticket: {} }
       taskbar2.state = { article: { body: 'some body' }, ticket: {} }
       taskbar2.save!
       taskbar2.save!
 
 
+      perform_enqueued_jobs
+
       taskbar1.reload
       taskbar1.reload
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
@@ -314,6 +324,8 @@ RSpec.describe Taskbar, type: :model do
       taskbar1.state = { article: { body: '' }, ticket: { state_id: 123 } }
       taskbar1.state = { article: { body: '' }, ticket: { state_id: 123 } }
       taskbar1.save!
       taskbar1.save!
 
 
+      perform_enqueued_jobs
+
       taskbar1.reload
       taskbar1.reload
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
@@ -357,6 +369,8 @@ RSpec.describe Taskbar, type: :model do
       taskbar2.notify = true
       taskbar2.notify = true
       taskbar2.save!
       taskbar2.save!
 
 
+      perform_enqueued_jobs
+
       taskbar1.reload
       taskbar1.reload
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
@@ -404,6 +418,8 @@ RSpec.describe Taskbar, type: :model do
       taskbar2.notify = true
       taskbar2.notify = true
       taskbar2.save!
       taskbar2.save!
 
 
+      perform_enqueued_jobs
+
       taskbar1.reload
       taskbar1.reload
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks].count).to eq(3)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
       expect(taskbar1.preferences[:tasks][0][:user_id]).to eq(1)
@@ -515,6 +531,7 @@ RSpec.describe Taskbar, type: :model do
 
 
       it 'updates related items when creating a taskbar' do
       it 'updates related items when creating a taskbar' do
         create(:taskbar, key: key)
         create(:taskbar, key: key)
+        perform_enqueued_jobs
 
 
         expect(other_taskbar.reload.preferences[:tasks]).to include(include(user_id: other_user.id), include(user_id: 1))
         expect(other_taskbar.reload.preferences[:tasks]).to include(include(user_id: other_user.id), include(user_id: 1))
       end
       end
@@ -545,6 +562,8 @@ RSpec.describe Taskbar, type: :model do
 
 
         taskbar.update! state: { a: :b }
         taskbar.update! state: { a: :b }
 
 
+        perform_enqueued_jobs
+
         expect(other_taskbar.reload.preferences[:tasks]).to include(include(user_id: other_user.id), include(user_id: 1))
         expect(other_taskbar.reload.preferences[:tasks]).to include(include(user_id: other_user.id), include(user_id: 1))
       end
       end
 
 
@@ -619,10 +638,11 @@ RSpec.describe Taskbar, type: :model do
     before { taskbar_1 && taskbar_2 && taskbar_3 }
     before { taskbar_1 && taskbar_2 && taskbar_3 }
 
 
     it 'updates related taskbars' do
     it 'updates related taskbars' do
-      taskbar_1.send(:update_related_taskbars, { a: 1 })
+      taskbar_1.send(:update_related_taskbars)
+      perform_enqueued_jobs
 
 
-      expect(taskbar_2.reload).to have_attributes(preferences: { a: 1 })
-      expect(taskbar_3.reload).not_to have_attributes(preferences: { a: 1 })
+      expect(taskbar_2.reload.preferences[:tasks].count).to eq(2)
+      expect(taskbar_3.reload.preferences[:tasks].count).to eq(1)
     end
     end
   end
   end
 
 

+ 112 - 102
spec/system/apps/mobile_old/tickets/viewer/live_users_spec.rb

@@ -2,7 +2,7 @@
 
 
 require 'rails_helper'
 require 'rails_helper'
 
 
-RSpec.describe 'Mobile > Ticket > Viewers > Live Users', app: :mobile, authenticated_as: :agent, type: :system do
+RSpec.describe 'Mobile > Ticket > Viewers > Live Users', app: :mobile, authenticated_as: :agent, performs_jobs: true, type: :system do
   let(:group)         { Group.find_by(name: 'Users') }
   let(:group)         { Group.find_by(name: 'Users') }
   let(:agent)         { create(:agent, groups: [group]) }
   let(:agent)         { create(:agent, groups: [group]) }
   let(:another_agent) { create(:agent, groups: [group]) }
   let(:another_agent) { create(:agent, groups: [group]) }
@@ -32,151 +32,161 @@ RSpec.describe 'Mobile > Ticket > Viewers > Live Users', app: :mobile, authentic
 
 
   context 'when opening viewers', authenticated_as: :agent do
   context 'when opening viewers', authenticated_as: :agent do
     it 'shows the users currently looking at the ticket' do
     it 'shows the users currently looking at the ticket' do
-      taskbar_item = create(:taskbar, user_id: another_agent.id, key: "Ticket-#{ticket.id}", app: 'mobile')
-      open_viewers_dialog
-
-      # No idle viewers.
-      expect(page).to have_no_text('Opened in tabs')
-
-      # One active viewer, without editing.
-      expect(page)
-        .to have_text('Viewing ticket')
-        .and have_no_text(agent.fullname)
-        .and have_text(another_agent.fullname)
-        .and have_no_css('.icon.icon-edit')
-
-      # Checking pencil icon.
-      update_taskbar_item(taskbar_item, { editing: true }, another_agent.id, 2)
-
-      expect(page).to have_css('.icon.icon-edit')
-
-      # Checking idle.
-      travel_to 10.minutes.ago
-      update_taskbar_item(taskbar_item, { editing: false }, another_agent.id, 3)
-      travel_back
-
-      expect(page)
-        .to have_text('Opened in tabs')
-        .and have_no_text(agent.fullname)
-        .and have_no_text('Viewing ticket')
-        .and have_text(another_agent.fullname)
-        .and have_no_css('.icon.icon-edit')
+      perform_enqueued_jobs do
+        taskbar_item = create(:taskbar, user_id: another_agent.id, key: "Ticket-#{ticket.id}", app: 'mobile')
+        open_viewers_dialog
+
+        # No idle viewers.
+        expect(page).to have_no_text('Opened in tabs')
+
+        # One active viewer, without editing.
+        expect(page)
+          .to have_text('Viewing ticket')
+          .and have_no_text(agent.fullname)
+          .and have_text(another_agent.fullname)
+          .and have_no_css('.icon.icon-edit')
+
+        # Checking pencil icon.
+        update_taskbar_item(taskbar_item, { editing: true }, another_agent.id, 2)
+
+        expect(page).to have_css('.icon.icon-edit')
+
+        # Checking idle.
+        travel_to 10.minutes.ago
+        update_taskbar_item(taskbar_item, { editing: false }, another_agent.id, 3)
+        travel_back
+
+        expect(page)
+          .to have_text('Opened in tabs')
+          .and have_no_text(agent.fullname)
+          .and have_no_text('Viewing ticket')
+          .and have_text(another_agent.fullname)
+          .and have_no_css('.icon.icon-edit')
+      end
     end
     end
 
 
     it 'shows the users that start looking at the ticket' do
     it 'shows the users that start looking at the ticket' do
-      visit "/tickets/#{ticket.id}"
+      perform_enqueued_jobs do
+        visit "/tickets/#{ticket.id}"
 
 
-      expect(page).to have_no_button('Show ticket viewers')
+        expect(page).to have_no_button('Show ticket viewers')
 
 
-      taskbar_item = create(:taskbar, user_id: third_agent.id, key: "Ticket-#{ticket.id}", app: 'mobile')
-      update_taskbar_item(taskbar_item, { editing: true }, third_agent.id, 1)
-      open_viewers_dialog
+        taskbar_item = create(:taskbar, user_id: third_agent.id, key: "Ticket-#{ticket.id}", app: 'mobile')
+        update_taskbar_item(taskbar_item, { editing: true }, third_agent.id, 1)
+        open_viewers_dialog
 
 
-      expect(page)
-        .to have_text('Viewing ticket')
-        .and have_text(third_agent.fullname)
-        .and have_css('.icon.icon-edit')
+        expect(page)
+          .to have_text('Viewing ticket')
+          .and have_text(third_agent.fullname)
+          .and have_css('.icon.icon-edit')
+      end
     end
     end
 
 
     context 'when editing is started on mobile' do
     context 'when editing is started on mobile' do
       it 'updates the other session' do
       it 'updates the other session' do
-        visit "/tickets/#{ticket.id}"
+        perform_enqueued_jobs do
+          visit "/tickets/#{ticket.id}"
 
 
-        using_session(:customer) do
-          login(
-            username: another_agent.login,
-            password: 'test',
-          )
+          using_session(:customer) do
+            login(
+              username: another_agent.login,
+              password: 'test',
+            )
 
 
-          visit "/tickets/#{ticket.id}"
-        end
+            visit "/tickets/#{ticket.id}"
+          end
 
 
-        open_viewers_dialog
+          open_viewers_dialog
 
 
-        expect(page)
-          .to have_text(another_agent.fullname)
-          .and have_no_css('.icon.icon-edit')
+          expect(page)
+            .to have_text(another_agent.fullname)
+            .and have_no_css('.icon.icon-edit')
 
 
-        using_session(:customer) do
-          visit "/tickets/#{ticket.id}/information"
+          using_session(:customer) do
+            visit "/tickets/#{ticket.id}/information"
 
 
-          wait_for_form_to_settle('form-ticket-edit')
+            wait_for_form_to_settle('form-ticket-edit')
 
 
-          within_form(form_updater_gql_number: 1) do
-            find_input('Ticket title').type('New Title')
+            within_form(form_updater_gql_number: 1) do
+              find_input('Ticket title').type('New Title')
+            end
           end
           end
-        end
 
 
-        wait_for_viewers_subscription(number: 2)
+          wait_for_viewers_subscription(number: 2)
 
 
-        expect(page)
-          .to have_text('Viewing ticket')
-          .and have_text(another_agent.fullname)
-          .and have_css('.icon.icon-edit')
+          expect(page)
+            .to have_text('Viewing ticket')
+            .and have_text(another_agent.fullname)
+            .and have_css('.icon.icon-edit')
+        end
       end
       end
     end
     end
 
 
     context 'when editing is started on desktop' do
     context 'when editing is started on desktop' do
       it 'updates the other session' do
       it 'updates the other session' do
-        visit "/tickets/#{ticket.id}"
+        perform_enqueued_jobs do
+          visit "/tickets/#{ticket.id}"
 
 
-        using_session(:customer) do
-          login(
-            username:    another_agent.login,
-            password:    'test',
-            remember_me: false,
-            app:         :desktop,
-          )
+          using_session(:customer) do
+            login(
+              username:    another_agent.login,
+              password:    'test',
+              remember_me: false,
+              app:         :desktop,
+            )
 
 
-          visit "/#ticket/zoom/#{ticket.id}", app: :desktop
-        end
+            visit "/#ticket/zoom/#{ticket.id}", app: :desktop
+          end
 
 
-        open_viewers_dialog
+          open_viewers_dialog
 
 
-        expect(page)
-          .to have_text(another_agent.fullname)
-          .and have_no_css('.icon.icon-desktop-edit')
+          expect(page)
+            .to have_text(another_agent.fullname)
+            .and have_no_css('.icon.icon-desktop-edit')
 
 
-        using_session(:customer) do
-          within :active_content, '.tabsSidebar' do
-            select 'closed', from: 'State'
+          using_session(:customer) do
+            within :active_content, '.tabsSidebar' do
+              select 'closed', from: 'State'
+            end
           end
           end
-        end
 
 
-        wait_for_viewers_subscription(number: 2)
+          wait_for_viewers_subscription(number: 2)
 
 
-        expect(page)
-          .to have_text('Viewing ticket')
-          .and have_text(another_agent.fullname)
-          .and have_css('.icon.icon-desktop-edit')
+          expect(page)
+            .to have_text('Viewing ticket')
+            .and have_text(another_agent.fullname)
+            .and have_css('.icon.icon-desktop-edit')
+        end
       end
       end
     end
     end
 
 
     context 'when current user is using both desktop and mobile' do
     context 'when current user is using both desktop and mobile' do
       it 'shows correct icons' do
       it 'shows correct icons' do
-        visit "/tickets/#{ticket.id}"
+        perform_enqueued_jobs do
+          visit "/tickets/#{ticket.id}"
 
 
-        using_session(:customer) do
-          login(
-            username:    agent.login,
-            password:    'test',
-            remember_me: false,
-            app:         :desktop,
-          )
+          using_session(:customer) do
+            login(
+              username:    agent.login,
+              password:    'test',
+              remember_me: false,
+              app:         :desktop,
+            )
 
 
-          visit "/#ticket/zoom/#{ticket.id}", app: :desktop
+            visit "/#ticket/zoom/#{ticket.id}", app: :desktop
 
 
-          within :active_content, '.tabsSidebar' do
-            select 'closed', from: 'State'
+            within :active_content, '.tabsSidebar' do
+              select 'closed', from: 'State'
+            end
           end
           end
-        end
 
 
-        open_viewers_dialog
+          open_viewers_dialog
 
 
-        expect(page)
-          .to have_text('Viewing ticket')
-          .and have_text(agent.fullname)
-          .and have_css('.icon.icon-desktop-edit')
+          expect(page)
+            .to have_text('Viewing ticket')
+            .and have_text(agent.fullname)
+            .and have_css('.icon.icon-desktop-edit')
+        end
       end
       end
     end
     end
   end
   end

+ 117 - 89
spec/system/ticket/update/simultaneously_with_two_user_spec.rb

@@ -2,11 +2,13 @@
 
 
 require 'rails_helper'
 require 'rails_helper'
 
 
-RSpec.describe 'Ticket > Update > Simultaneously with two different user', type: :system do
+RSpec.describe 'Ticket > Update > Simultaneously with two different user', performs_jobs: true, type: :system do
   let(:group)  { Group.find_by(name: 'Users') }
   let(:group)  { Group.find_by(name: 'Users') }
   let(:ticket) { create(:ticket, group: group) }
   let(:ticket) { create(:ticket, group: group) }
   let(:agent)  { User.find_by(login: 'agent1@example.com') }
   let(:agent)  { User.find_by(login: 'agent1@example.com') }
 
 
+  around { |example| perform_enqueued_jobs { example.run } }
+
   # rubocop:disable RSpec/InstanceVariable
   # rubocop:disable RSpec/InstanceVariable
   define :have_avatar do |expected|
   define :have_avatar do |expected|
     chain(:changed, :text)
     chain(:changed, :text)
@@ -95,135 +97,143 @@ RSpec.describe 'Ticket > Update > Simultaneously with two different user', type:
     end
     end
 
 
     it 'avatar from other user should be visible in ticket zoom' do
     it 'avatar from other user should be visible in ticket zoom' do
-      expect(page).to have_avatar('AT')
+      perform_enqueued_jobs do
+        expect(page).to have_avatar('AT')
 
 
-      using_session(:second_browser) do
-        expect(page).to have_avatar('TA')
+        using_session(:second_browser) do
+          expect(page).to have_avatar('TA')
+        end
       end
       end
     end
     end
 
 
     it 'check changes from the first user and added changes from the second user' do
     it 'check changes from the first user and added changes from the second user' do
-      within(:active_content) do
-        find('.js-textarea').send_keys('some note')
+      perform_enqueued_jobs do
+        within(:active_content) do
+          find('.js-textarea').send_keys('some note')
 
 
-        expect(page).to have_css('.js-reset')
-      end
+          expect(page).to have_css('.js-reset')
+        end
 
 
-      expect(page).to have_avatar('AT')
+        expect(page).to have_avatar('AT')
 
 
-      using_session(:second_browser) do
-        expect(page).to have_avatar('TA').changed!
+        using_session(:second_browser) do
+          expect(page).to have_avatar('TA').changed!
 
 
-        within(:active_content) do
-          find('.js-textarea').send_keys('some other note')
+          within(:active_content) do
+            find('.js-textarea').send_keys('some other note')
 
 
-          expect(page).to have_css('.js-reset')
+            expect(page).to have_css('.js-reset')
+          end
         end
         end
-      end
 
 
-      expect(page).to have_avatar('AT').changed!
+        expect(page).to have_avatar('AT').changed!
 
 
-      using_session(:second_browser) do
-        within(:active_content) do
-          click '.js-attributeBar .js-submit'
+        using_session(:second_browser) do
+          within(:active_content) do
+            click '.js-attributeBar .js-submit'
 
 
-          expect(page).to have_no_css('.js-reset')
-          expect(page).to have_css('.article-content', text: 'some other note')
+            expect(page).to have_no_css('.js-reset')
+            expect(page).to have_css('.article-content', text: 'some other note')
+          end
+
+          expect(page).to have_avatar('TA').changed!
         end
         end
 
 
-        expect(page).to have_avatar('TA').changed!
-      end
+        expect(page).to have_avatar('AT')
+        check_taskbar_tab(ticket.id, title: ticket.title, modified: true)
 
 
-      expect(page).to have_avatar('AT')
-      check_taskbar_tab(ticket.id, title: ticket.title, modified: true)
+        within(:active_content) do
+          expect(page).to have_css('.article-content', text: 'some other note')
 
 
-      within(:active_content) do
-        expect(page).to have_css('.article-content', text: 'some other note')
+          click '.js-attributeBar .js-submit'
 
 
-        click '.js-attributeBar .js-submit'
+          expect(page).to have_no_css('.js-reset')
+          expect(page).to have_css('.article-content', text: 'some note')
+        end
 
 
-        expect(page).to have_no_css('.js-reset')
-        expect(page).to have_css('.article-content', text: 'some note')
-      end
+        using_session(:second_browser) do
+          expect(page).to have_avatar('TA')
 
 
-      using_session(:second_browser) do
-        expect(page).to have_avatar('TA')
+          expect(page).to have_css('.article-content', text: 'some note')
+          check_taskbar_tab(ticket.id, title: ticket.title, modified: true)
+        end
 
 
-        expect(page).to have_css('.article-content', text: 'some note')
-        check_taskbar_tab(ticket.id, title: ticket.title, modified: true)
-      end
+        # Reload browsers and check if state is correct.
+        refresh
 
 
-      # Reload browsers and check if state is correct.
-      refresh
+        using_session(:second_browser) do
+          refresh
 
 
-      using_session(:second_browser) do
-        refresh
+          expect(page).to have_avatar('TA')
+          expect(page).to have_no_css('.js-reset')
+        end
 
 
-        expect(page).to have_avatar('TA')
+        expect(page).to have_avatar('AT')
         expect(page).to have_no_css('.js-reset')
         expect(page).to have_no_css('.js-reset')
       end
       end
-
-      expect(page).to have_avatar('AT')
-      expect(page).to have_no_css('.js-reset')
     end
     end
 
 
     it 'check refresh for unsaved changes and reset after refresh' do
     it 'check refresh for unsaved changes and reset after refresh' do
-      using_session(:second_browser) do
-        within(:active_content) do
-          find('.js-textarea').send_keys('some other note')
+      perform_enqueued_jobs do
+        using_session(:second_browser) do
+          within(:active_content) do
+            find('.js-textarea').send_keys('some other note')
 
 
-          expect(page).to have_css('.js-reset')
-        end
+            expect(page).to have_css('.js-reset')
+          end
 
 
-        expect(page).to have_avatar('TA')
+          expect(page).to have_avatar('TA')
 
 
-        # We need to wait for the auto save feature.
-        wait.until do
-          Taskbar.find_by(key: "Ticket-#{ticket.id}", user_id: agent.id).state_changed?
-        end
+          # We need to wait for the auto save feature.
+          wait.until do
+            Taskbar.find_by(key: "Ticket-#{ticket.id}", user_id: agent.id).state_changed?
+          end
 
 
-        refresh
-      end
+          refresh
+        end
 
 
-      expect(page).to have_avatar('AT').changed!
+        expect(page).to have_avatar('AT').changed!
 
 
-      using_session(:second_browser) do
-        refresh
+        using_session(:second_browser) do
+          refresh
 
 
-        within(:active_content) do
-          click '.js-reset'
-          expect(page).to have_css('.js-textarea', text: '')
+          within(:active_content) do
+            click '.js-reset'
+            expect(page).to have_css('.js-textarea', text: '')
+          end
         end
         end
-      end
 
 
-      expect(page).to have_avatar('AT')
+        expect(page).to have_avatar('AT')
+      end
     end
     end
 
 
     it 'change title with second user' do
     it 'change title with second user' do
-      find('.js-textarea').send_keys('some note')
+      perform_enqueued_jobs do
+        find('.js-textarea').send_keys('some note')
 
 
-      using_session(:second_browser) do
-        find('.js-textarea').send_keys('some other note')
-        find('.ticketZoom-header .js-objectTitle').set('TTTsome level 2 <b>subject</b> 123äöü')
+        using_session(:second_browser) do
+          find('.js-textarea').send_keys('some other note')
+          find('.ticketZoom-header .js-objectTitle').set('TTTsome level 2 <b>subject</b> 123äöü')
 
 
-        # Click in the body field, to trigger the title update.
-        find('.js-textarea').send_keys('trigger title')
+          # Click in the body field, to trigger the title update.
+          find('.js-textarea').send_keys('trigger title')
 
 
-        expect(page).to have_css('.js-objectTitle', text: 'TTTsome level 2 <b>subject</b> 123äöü')
+          expect(page).to have_css('.js-objectTitle', text: 'TTTsome level 2 <b>subject</b> 123äöü')
 
 
-        check_taskbar_tab(ticket.id, title: 'TTTsome level 2 <b>subject</b> 123äöü')
+          check_taskbar_tab(ticket.id, title: 'TTTsome level 2 <b>subject</b> 123äöü')
 
 
-        expect(page).to have_css('.js-textarea', text: 'some other note')
-      end
+          expect(page).to have_css('.js-textarea', text: 'some other note')
+        end
 
 
-      expect(page).to have_css('.js-objectTitle', text: 'TTTsome level 2 <b>subject</b> 123äöü')
-      expect(page).to have_css('.js-textarea', text: 'some note')
+        expect(page).to have_css('.js-objectTitle', text: 'TTTsome level 2 <b>subject</b> 123äöü')
+        expect(page).to have_css('.js-textarea', text: 'some note')
 
 
-      check_taskbar_tab(ticket.id, title: 'TTTsome level 2 <b>subject</b> 123äöü', modified: true)
+        check_taskbar_tab(ticket.id, title: 'TTTsome level 2 <b>subject</b> 123äöü', modified: true)
 
 
-      # Refresh and check that modified flag is gone
-      refresh
-      check_taskbar_tab(ticket.id, title: 'TTTsome level 2 <b>subject</b> 123äöü', modified: false)
+        # Refresh and check that modified flag is gone
+        refresh
+        check_taskbar_tab(ticket.id, title: 'TTTsome level 2 <b>subject</b> 123äöü', modified: false)
+      end
     end
     end
   end
   end
 
 
@@ -248,7 +258,9 @@ RSpec.describe 'Ticket > Update > Simultaneously with two different user', type:
       end
       end
 
 
       it 'does not show current user' do
       it 'does not show current user' do
-        expect(page).not_to have_avatar(user)
+        perform_enqueued_jobs do
+          expect(page).not_to have_avatar(user)
+        end
       end
       end
     end
     end
 
 
@@ -261,7 +273,9 @@ RSpec.describe 'Ticket > Update > Simultaneously with two different user', type:
       end
       end
 
 
       it 'shows another user' do
       it 'shows another user' do
-        expect(page).to have_avatar(another_user).with_no_icon!
+        perform_enqueued_jobs do
+          expect(page).to have_avatar(another_user).with_no_icon!
+        end
       end
       end
     end
     end
 
 
@@ -274,7 +288,9 @@ RSpec.describe 'Ticket > Update > Simultaneously with two different user', type:
       end
       end
 
 
       it 'shows another user' do
       it 'shows another user' do
-        expect(page).to have_avatar(another_user).with_icon(:mobile)
+        perform_enqueued_jobs do
+          expect(page).to have_avatar(another_user).with_icon(:mobile)
+        end
       end
       end
     end
     end
 
 
@@ -288,7 +304,9 @@ RSpec.describe 'Ticket > Update > Simultaneously with two different user', type:
       end
       end
 
 
       it 'shows another user' do
       it 'shows another user' do
-        expect(page).to have_avatar(another_user).with_no_icon!
+        perform_enqueued_jobs do
+          expect(page).to have_avatar(another_user).with_no_icon!
+        end
       end
       end
     end
     end
 
 
@@ -301,7 +319,9 @@ RSpec.describe 'Ticket > Update > Simultaneously with two different user', type:
       end
       end
 
 
       it 'shows another user' do
       it 'shows another user' do
-        expect(page).to have_avatar(another_user).with_icon(:pen).changed!
+        perform_enqueued_jobs do
+          expect(page).to have_avatar(another_user).with_icon(:pen).changed!
+        end
       end
       end
     end
     end
 
 
@@ -314,7 +334,9 @@ RSpec.describe 'Ticket > Update > Simultaneously with two different user', type:
       end
       end
 
 
       it 'shows another user' do
       it 'shows another user' do
-        expect(page).to have_avatar(another_user).with_icon(:pen).changed!
+        perform_enqueued_jobs do
+          expect(page).to have_avatar(another_user).with_icon(:pen).changed!
+        end
       end
       end
     end
     end
 
 
@@ -327,7 +349,9 @@ RSpec.describe 'Ticket > Update > Simultaneously with two different user', type:
       end
       end
 
 
       it 'shows same user' do
       it 'shows same user' do
-        expect(page).not_to have_avatar(user)
+        perform_enqueued_jobs do
+          expect(page).not_to have_avatar(user)
+        end
       end
       end
     end
     end
 
 
@@ -339,7 +363,9 @@ RSpec.describe 'Ticket > Update > Simultaneously with two different user', type:
       end
       end
 
 
       it 'do not show same user' do
       it 'do not show same user' do
-        expect(page).not_to have_avatar(user)
+        perform_enqueued_jobs do
+          expect(page).not_to have_avatar(user)
+        end
       end
       end
     end
     end
 
 
@@ -352,7 +378,9 @@ RSpec.describe 'Ticket > Update > Simultaneously with two different user', type:
       end
       end
 
 
       it 'shows same user' do
       it 'shows same user' do
-        expect(page).to have_avatar(user).with_icon(:'mobile-edit').changed!
+        perform_enqueued_jobs do
+          expect(page).to have_avatar(user).with_icon(:'mobile-edit').changed!
+        end
       end
       end
     end
     end
   end
   end