message_train 0.7.6 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. checksums.yaml +5 -5
  2. data/.gitattributes +1 -0
  3. data/.github/workflows/ci.yml +86 -0
  4. data/.gitignore +64 -0
  5. data/.rubocop.yml +9 -0
  6. data/.rubocop_todo.yml +854 -2
  7. data/.ruby-version +1 -1
  8. data/.simplecov +2 -4
  9. data/CHANGELOG.md +128 -0
  10. data/CLAUDE.md +5 -0
  11. data/Gemfile +29 -31
  12. data/LICENSE.txt +1 -1
  13. data/README.md +106 -35
  14. data/Rakefile +4 -18
  15. data/app/assets/stylesheets/message_train.scss +405 -67
  16. data/app/controllers/concerns/message_train_support.rb +2 -2
  17. data/app/controllers/message_train/boxes_controller.rb +4 -2
  18. data/app/controllers/message_train/conversations_controller.rb +1 -0
  19. data/app/helpers/message_train/application_helper.rb +68 -0
  20. data/app/helpers/message_train/attachments_helper.rb +7 -4
  21. data/app/helpers/message_train/conversations_helper.rb +16 -16
  22. data/app/helpers/message_train/messages_helper.rb +23 -10
  23. data/app/javascript/controllers/message_train/bulk_actions_controller.js +36 -0
  24. data/app/javascript/controllers/message_train/lightbox_controller.js +25 -0
  25. data/app/javascript/controllers/message_train/nested_form_controller.js +36 -0
  26. data/app/javascript/controllers/message_train/recipient_autocomplete_controller.js +31 -0
  27. data/app/mailers/message_train/previews/receipt_mailer_preview.rb +7 -5
  28. data/app/models/message_train/attachment.rb +54 -42
  29. data/app/models/message_train/box.rb +2 -2
  30. data/app/models/message_train/conversation.rb +28 -2
  31. data/app/models/message_train/ignore.rb +2 -2
  32. data/app/models/message_train/message.rb +10 -4
  33. data/app/models/message_train/receipt.rb +27 -4
  34. data/app/models/message_train/unsubscribe.rb +1 -1
  35. data/app/views/application/404.html.haml +1 -3
  36. data/app/views/kaminari/_first_page.html.haml +1 -1
  37. data/app/views/kaminari/_last_page.html.haml +1 -1
  38. data/app/views/kaminari/_next_page.html.haml +1 -1
  39. data/app/views/kaminari/_page.html.haml +3 -3
  40. data/app/views/kaminari/_paginator.html.haml +12 -11
  41. data/app/views/kaminari/_prev_page.html.haml +1 -1
  42. data/app/views/message_train/application/_attachment_fields.html.haml +10 -7
  43. data/app/views/message_train/application/_attachment_link.html.haml +2 -2
  44. data/app/views/message_train/application/_widget.html.haml +2 -4
  45. data/app/views/message_train/boxes/_widget.html.haml +5 -5
  46. data/app/views/message_train/boxes/show.html.haml +28 -45
  47. data/app/views/message_train/collectives/_widget.html.haml +4 -4
  48. data/app/views/message_train/conversations/_conversation.html.haml +11 -11
  49. data/app/views/message_train/conversations/_deleted_toggle.html.haml +1 -1
  50. data/app/views/message_train/conversations/_ignored_toggle.html.haml +2 -2
  51. data/app/views/message_train/conversations/_read_toggle.html.haml +2 -2
  52. data/app/views/message_train/conversations/_toggle.html.haml +6 -4
  53. data/app/views/message_train/conversations/_trashed_toggle.html.haml +2 -2
  54. data/app/views/message_train/conversations/show.html.haml +8 -6
  55. data/app/views/message_train/messages/_deleted_toggle.html.haml +1 -1
  56. data/app/views/message_train/messages/_form.html.haml +22 -30
  57. data/app/views/message_train/messages/_message.html.haml +27 -45
  58. data/app/views/message_train/messages/_read_toggle.html.haml +2 -2
  59. data/app/views/message_train/messages/_toggle.html.haml +5 -1
  60. data/app/views/message_train/messages/_trashed_toggle.html.haml +2 -2
  61. data/app/views/message_train/unsubscribes/index.html.haml +12 -15
  62. data/badges/coverage.json +1 -0
  63. data/config/importmap.rb +2 -0
  64. data/config/locales/en.yml +6 -3
  65. data/db/migrate/20150721145319_create_message_train_conversations.rb +1 -1
  66. data/db/migrate/20150721160322_create_message_train_messages.rb +1 -1
  67. data/db/migrate/20150721161144_create_message_train_attachments.rb +5 -2
  68. data/db/migrate/20150721161940_create_message_train_receipts.rb +1 -1
  69. data/db/migrate/20150721163838_create_message_train_ignores.rb +1 -1
  70. data/db/migrate/20150901183458_add_received_through_to_message_train_receipts.rb +1 -1
  71. data/db/migrate/20151004184347_add_unique_index_to_receipts.rb +1 -1
  72. data/db/migrate/20151124000820_create_message_train_unsubscribes.rb +1 -1
  73. data/db/migrate/20260827095938_remove_paperclip_columns_from_message_train_attachments.rb +8 -0
  74. data/db/migrate/20260827171549_remove_body_from_message_train_messages.rb +5 -0
  75. data/db/migrate/20260827180000_add_marking_indexes.rb +9 -0
  76. data/db/migrate/20260827200000_add_body_back_to_message_train_messages.rb +5 -0
  77. data/docs/screenshots/inbox-desktop.png +0 -0
  78. data/docs/screenshots/inbox-mobile.png +0 -0
  79. data/lib/generators/message_train/install/install_generator.rb +1 -1
  80. data/lib/message_train/engine.rb +11 -0
  81. data/lib/message_train/instance_methods.rb +13 -7
  82. data/lib/message_train/localization.rb +4 -3
  83. data/lib/message_train/version.rb +1 -1
  84. data/lib/message_train.rb +0 -2
  85. data/message_train.gemspec +57 -389
  86. data/spec/controllers/message_train/boxes_controller_spec.rb +13 -8
  87. data/spec/controllers/message_train/concerns_spec.rb +2 -2
  88. data/spec/controllers/message_train/conversations_controller_spec.rb +36 -7
  89. data/spec/controllers/message_train/messages_controller_spec.rb +4 -4
  90. data/spec/dummy/app/assets/stylesheets/application.scss +141 -12
  91. data/spec/dummy/app/controllers/application_controller.rb +1 -1
  92. data/spec/dummy/app/javascript/application.js +4 -0
  93. data/spec/dummy/app/javascript/controllers/application.js +9 -0
  94. data/spec/dummy/app/javascript/controllers/index.js +4 -0
  95. data/spec/dummy/app/models/role.rb +1 -1
  96. data/spec/dummy/app/views/active_storage/blobs/_blob.html.erb +14 -0
  97. data/spec/dummy/app/views/layouts/_top_navigation.html.haml +18 -7
  98. data/spec/dummy/app/views/layouts/application.html.haml +22 -26
  99. data/spec/dummy/bin/importmap +4 -0
  100. data/spec/dummy/config/application.rb +7 -20
  101. data/spec/dummy/config/environments/development.rb +17 -21
  102. data/spec/dummy/config/environments/production.rb +11 -63
  103. data/spec/dummy/config/environments/test.rb +21 -16
  104. data/spec/dummy/config/importmap.rb +7 -0
  105. data/spec/dummy/config/initializers/assets.rb +1 -6
  106. data/spec/dummy/config/routes.rb +1 -1
  107. data/spec/dummy/config/storage.yml +7 -0
  108. data/spec/dummy/db/migrate/20150721140013_devise_create_users.rb +1 -1
  109. data/spec/dummy/db/migrate/20150721141009_rolify_create_roles.rb +1 -1
  110. data/spec/dummy/db/migrate/20150721141128_create_groups.rb +1 -1
  111. data/spec/dummy/db/migrate/20150721150307_add_display_name_column_to_users.rb +1 -1
  112. data/spec/dummy/db/migrate/20160207190402_create_message_train_conversations.message_train.rb +1 -1
  113. data/spec/dummy/db/migrate/20160207190403_create_message_train_messages.message_train.rb +1 -1
  114. data/spec/dummy/db/migrate/20160207190404_create_message_train_attachments.message_train.rb +5 -2
  115. data/spec/dummy/db/migrate/20160207190405_create_message_train_receipts.message_train.rb +1 -1
  116. data/spec/dummy/db/migrate/20160207190406_create_message_train_ignores.message_train.rb +1 -1
  117. data/spec/dummy/db/migrate/20160207190407_add_received_through_to_message_train_receipts.message_train.rb +1 -1
  118. data/spec/dummy/db/migrate/20160207190408_add_unique_index_to_receipts.message_train.rb +1 -1
  119. data/spec/dummy/db/migrate/20160207190409_create_message_train_unsubscribes.message_train.rb +1 -1
  120. data/spec/dummy/db/migrate/20260827095939_remove_paperclip_columns_from_message_train_attachments.message_train.rb +9 -0
  121. data/spec/dummy/db/migrate/20260827161141_create_active_storage_tables.active_storage.rb +57 -0
  122. data/spec/dummy/db/migrate/20260827171550_remove_body_from_message_train_messages.message_train.rb +6 -0
  123. data/spec/dummy/db/migrate/20260827225116_add_marking_indexes.message_train.rb +10 -0
  124. data/spec/dummy/db/migrate/20260828034830_add_body_back_to_message_train_messages.message_train.rb +6 -0
  125. data/spec/dummy/db/migrate/20260828040000_drop_action_text_tables.rb +5 -0
  126. data/spec/dummy/db/schema.rb +106 -72
  127. data/spec/dummy/db/seeds/conversations.seeds.rb +22 -22
  128. data/spec/dummy/db/seeds/development/conversations.seeds.rb +2 -2
  129. data/spec/dummy/db/seeds/groups.seeds.rb +4 -4
  130. data/spec/dummy/db/seeds/test/attachments.seeds.rb +4 -4
  131. data/spec/dummy/db/seeds/test/conversations.seeds.rb +2 -2
  132. data/spec/dummy/db/seeds/users.seeds.rb +8 -8
  133. data/spec/dummy/vendor/javascript/.keep +0 -0
  134. data/spec/factories/attachment.rb +5 -6
  135. data/spec/factories/group.rb +2 -2
  136. data/spec/factories/message.rb +7 -7
  137. data/spec/factories/user.rb +3 -3
  138. data/spec/features/boxes_spec.rb +38 -34
  139. data/spec/features/conversations_spec.rb +13 -6
  140. data/spec/features/messages_spec.rb +20 -20
  141. data/spec/features/unsubscribes_spec.rb +9 -9
  142. data/spec/helpers/message_train/application_helper_spec.rb +16 -18
  143. data/spec/helpers/message_train/attachment_helper_spec.rb +8 -18
  144. data/spec/helpers/message_train/boxes_helper_spec.rb +6 -6
  145. data/spec/helpers/message_train/collectives_helper_spec.rb +10 -10
  146. data/spec/helpers/message_train/conversations_helper_spec.rb +43 -85
  147. data/spec/helpers/message_train/messages_helper_spec.rb +41 -61
  148. data/spec/models/message_train/attachment_spec.rb +26 -36
  149. data/spec/models/message_train/box_spec.rb +2 -2
  150. data/spec/models/message_train/conversation_spec.rb +15 -0
  151. data/spec/models/message_train/message_spec.rb +5 -1
  152. data/spec/models/message_train/receipt_spec.rb +1 -1
  153. data/spec/models/message_train/unsubscribe_spec.rb +1 -1
  154. data/spec/rails_helper.rb +14 -13
  155. data/spec/support/controller_behaviors.rb +13 -13
  156. data/spec/support/factory_bot.rb +3 -0
  157. data/spec/support/feature_behaviors.rb +14 -14
  158. data/spec/support/loaded_site/attachments.rb +6 -2
  159. data/spec/support/utilities.rb +49 -20
  160. metadata +260 -213
  161. data/.travis.yml +0 -12
  162. data/VERSION +0 -1
  163. data/app/assets/javascripts/ckeditor/config.js +0 -49
  164. data/app/assets/javascripts/message_train.js +0 -149
  165. data/spec/dummy/app/assets/javascripts/application.js +0 -17
  166. data/spec/dummy/app/assets/stylesheets/bootstrap-everything.scss +0 -54
  167. data/spec/dummy/config/initializers/bootstrap_leather.rb +0 -7
  168. data/spec/dummy/config/initializers/paperclip.rb +0 -5
  169. data/spec/dummy/config/secrets.yml +0 -22
  170. data/spec/dummy/db/test.sqlite3 +0 -0
  171. data/spec/support/factory_girl.rb +0 -3
@@ -8,7 +8,7 @@ module MessageTrain
8
8
  # Relationships
9
9
  it { should belong_to :message }
10
10
  it { should belong_to :recipient }
11
- it { should belong_to :received_through }
11
+ it { should belong_to(:received_through).optional }
12
12
  # Validations
13
13
  it { should validate_presence_of :message }
14
14
  it { should validate_presence_of :recipient }
@@ -7,7 +7,7 @@ module MessageTrain
7
7
  describe 'Model' do
8
8
  # Relationships
9
9
  it { should belong_to :recipient }
10
- it { should belong_to :from }
10
+ it { should belong_to(:from).optional }
11
11
  # Validations
12
12
  it { should validate_presence_of :recipient }
13
13
  end
data/spec/rails_helper.rb CHANGED
@@ -1,5 +1,4 @@
1
- require 'coveralls'
2
- Coveralls.wear!
1
+ require 'simplecov'
3
2
 
4
3
  # This file is copied to spec/ when you run 'rails generate rspec:install'
5
4
  ENV['RAILS_ENV'] ||= 'test'
@@ -12,11 +11,9 @@ require 'spec_helper'
12
11
  require 'rspec/rails'
13
12
  # Add additional requires below this line. Rails is not loaded until this point!
14
13
  require 'shoulda/matchers'
15
- require 'factory_girl_rails'
16
- require 'paperclip/matchers'
17
14
  require 'capybara/rspec'
18
- require 'capybara/poltergeist'
19
- require 'database_cleaner'
15
+ require 'capybara/cuprite'
16
+ require 'database_cleaner/active_record'
20
17
  require 'rake'
21
18
 
22
19
  # Requires supporting ruby files with custom matchers and macros, etc, in
@@ -42,7 +39,10 @@ Dir[MessageTrain::Engine.root.join('spec/support/**/*.rb')]
42
39
  ActiveRecord::Migrator.migrations_paths = 'spec/dummy/db/migrate'
43
40
  ActiveRecord::Migration.maintain_test_schema!
44
41
 
45
- Capybara.javascript_driver = :poltergeist
42
+ Capybara.register_driver(:cuprite) do |app|
43
+ Capybara::Cuprite::Driver.new(app, window_size: [1200, 800], process_timeout: 15)
44
+ end
45
+ Capybara.javascript_driver = :cuprite
46
46
 
47
47
  Shoulda::Matchers.configure do |config|
48
48
  config.integrate do |with|
@@ -57,6 +57,12 @@ RSpec.configure do |config|
57
57
  config.before(:suite) do
58
58
  DatabaseCleaner.strategy = :transaction
59
59
  DatabaseCleaner.clean_with(:truncation)
60
+ # Seedbank resolves seed files relative to Rake's process cwd
61
+ # (Rake.application.original_dir), not Rails.root, and memoizes that on
62
+ # first access. Since specs run from the gem root, not spec/dummy, this
63
+ # must be set before Dummy::Application.load_tasks triggers Seedbank's
64
+ # own task generation, or it silently finds no seed files at all.
65
+ Seedbank.application_root = MessageTrain::Engine.root.join('spec/dummy')
60
66
  Dummy::Application.load_tasks
61
67
  Rake::Task['db:seed'].invoke # loading seeds
62
68
  end
@@ -82,9 +88,6 @@ RSpec.configure do |config|
82
88
  # https://relishapp.com/rspec/rspec-rails/docs
83
89
  config.infer_spec_type_from_file_location!
84
90
 
85
- config.expect_with(:rspec) { |c| c.syntax = [:should, :expect] }
86
- config.mock_with(:rspec) { |c| c.syntax = [:should, :expect] }
87
-
88
91
  config.before(:each) { @routes = MessageTrain::Engine.routes }
89
92
  config.include MessageTrain::Engine.routes.url_helpers
90
93
 
@@ -94,9 +97,7 @@ RSpec.configure do |config|
94
97
  config.include Devise::Test::ControllerHelpers, type: :routing
95
98
  config.include RSpecHtmlMatchers
96
99
 
97
- # config.include(Shoulda::Matchers::ActiveRecord, type: :model)
98
- config.include Paperclip::Shoulda::Matchers
99
100
  config.after(:suite) do
100
- FileUtils.rm_rf(Dir["#{Rails.root}/public/system/test/*/*"])
101
+ FileUtils.rm_rf(Dir["#{Rails.root}/tmp/storage/*"])
101
102
  end
102
103
  end
@@ -1,7 +1,7 @@
1
1
  shared_examples_for 'a successful page' do |options = {}|
2
2
  context 'responds successfully' do
3
3
  subject { response }
4
- it { should be_success }
4
+ it { is_expected.to be_successful }
5
5
  end
6
6
  if options[:which_renders].present?
7
7
  it_behaves_like 'a page rendering a template', options[:which_renders]
@@ -10,25 +10,25 @@ end
10
10
  shared_examples_for 'a page rendering a template' do |template|
11
11
  context "renders the #{template} template" do
12
12
  subject { response }
13
- it { should render_template(template) }
13
+ it { is_expected.to render_template(template) }
14
14
  end
15
15
  end
16
16
  shared_examples_for 'an error response' do |http_status|
17
17
  context "issues error response code #{http_status}" do
18
18
  subject { response }
19
- it { should have_http_status(http_status) }
19
+ it { is_expected.to have_http_status(http_status) }
20
20
  end
21
21
  end
22
22
  shared_examples_for 'a redirect to' do |path|
23
23
  context "redirects to #{path}" do
24
24
  subject { response }
25
- it { should redirect_to path_to_url(path) }
25
+ it { is_expected.to redirect_to path_to_url(path) }
26
26
  end
27
27
  end
28
28
  shared_examples_for 'a redirect matching' do |path_expression|
29
29
  context "redirects matching #{path_expression}" do
30
30
  subject { response.location }
31
- it { should match path_expression }
31
+ it { is_expected.to match path_expression }
32
32
  end
33
33
  end
34
34
  shared_examples_for 'a redirect to sign in' do
@@ -39,7 +39,7 @@ shared_examples_for 'a redirect with a message' do |path, options|
39
39
  options.each do |key, value|
40
40
  context "sets #{key} message" do
41
41
  subject { flash[key] }
42
- it { should eq value }
42
+ it { is_expected.to eq value }
43
43
  end
44
44
  end
45
45
  end
@@ -47,7 +47,7 @@ shared_examples_for 'an error response with message' do |message|
47
47
  it_behaves_like 'an error response', :forbidden
48
48
  context 'sets failure message' do
49
49
  subject { flash[:error] }
50
- it { should eq message }
50
+ it { is_expected.to eq message }
51
51
  end
52
52
  end
53
53
  shared_examples_for 'a 404 Not Found error' do
@@ -59,7 +59,7 @@ shared_examples_for 'a page with a message' do |options|
59
59
  options.each do |key, value|
60
60
  context "sets #{key} message" do
61
61
  subject { flash[key] }
62
- it { should eq value }
62
+ it { is_expected.to eq value }
63
63
  end
64
64
  end
65
65
  end
@@ -68,7 +68,7 @@ shared_examples_for 'a page with a message matching' do |options|
68
68
  options.each do |key, value|
69
69
  context "sets #{key} message" do
70
70
  subject { flash[key] }
71
- it { should match value }
71
+ it { is_expected.to match value }
72
72
  end
73
73
  end
74
74
  end
@@ -94,26 +94,26 @@ shared_examples_for 'a redirect with error' do |path, message|
94
94
  it_behaves_like 'a redirect to', path
95
95
  context 'sets error' do
96
96
  subject { flash[:error] }
97
- it { should eq message }
97
+ it { is_expected.to eq message }
98
98
  end
99
99
  end
100
100
  shared_examples_for 'a redirect with notice' do |path, message|
101
101
  it_behaves_like 'a redirect to', path
102
102
  context 'sets notice' do
103
103
  subject { flash[:notice] }
104
- it { should eq message }
104
+ it { is_expected.to eq message }
105
105
  end
106
106
  end
107
107
  shared_examples_for 'a redirect with alert' do |path, message|
108
108
  it_behaves_like 'a redirect to', path
109
109
  context 'sets alert' do
110
110
  subject { flash[:alert] }
111
- it { should eq message }
111
+ it { is_expected.to eq message }
112
112
  end
113
113
  end
114
114
  shared_examples_for 'a response without error' do
115
115
  context 'sets no errors' do
116
116
  subject { flash[:error] }
117
- it { should be nil }
117
+ it { is_expected.to be_nil }
118
118
  end
119
119
  end
@@ -0,0 +1,3 @@
1
+ RSpec.configure do |config|
2
+ config.include FactoryBot::Syntax::Methods
3
+ end
@@ -10,8 +10,8 @@ shared_examples_for 'an authenticated section' do |path|
10
10
  )
11
11
  context 'displays a login form' do
12
12
  subject { page }
13
- it { should have_selector 'form.new_user input#user_email' }
14
- it { should have_selector 'form.new_user input#user_password' }
13
+ it { is_expected.to have_selector 'form.new_user input#user_email' }
14
+ it { is_expected.to have_selector 'form.new_user input#user_password' }
15
15
  end
16
16
  end
17
17
  end
@@ -21,14 +21,14 @@ shared_examples_for 'a bootstrap page' do |options = {}|
21
21
  context 'displays a page with bootstrap elements' do
22
22
  subject { page }
23
23
  if options[:title].present?
24
- it { should have_title html_escape(options[:title]) }
25
- it { should have_xpath '//h1', text: options[:title] }
24
+ it { is_expected.to have_title html_escape(options[:title]) }
25
+ it { is_expected.to have_xpath '//h2', text: options[:title] }
26
26
  end
27
27
  it do
28
- should(
28
+ expect(page).to(
29
29
  have_selector(
30
- '.navbar .navbar-header .navbar-brand',
31
- text: BootstrapLeather.configuration.application_title
30
+ '.top-navigation .site-brand',
31
+ text: MessageTrain.configuration.site_name
32
32
  )
33
33
  )
34
34
  end
@@ -40,21 +40,21 @@ shared_examples_for(
40
40
  ) do |options|
41
41
  if options[:text]
42
42
  it do
43
- should have_selector 'li.dropdown a.dropdown-toggle', text: options[:text]
43
+ expect(page).to have_selector 'details.message-train-dropdown-nav-item summary', text: options[:text]
44
44
  end
45
45
  else
46
- it { should have_selector 'li.dropdown a.dropdown-toggle' }
46
+ it { is_expected.to have_selector 'details.message-train-dropdown-nav-item summary' }
47
47
  end
48
48
  end
49
49
 
50
50
  shared_examples_for 'a bootstrap page with an alert' do |type, text|
51
51
  subject { page }
52
- it { should have_selector ".alert.alert-#{type}", text: text }
52
+ it { is_expected.to have_selector "#alert_area .alert.#{type}", text: text }
53
53
  end
54
54
 
55
55
  shared_examples_for 'a bootstrap page without an alert' do |type|
56
56
  subject { page }
57
- it { should_not have_selector ".alert.alert-#{type}" }
57
+ it { is_expected.not_to have_selector "#alert_area .alert.#{type}" }
58
58
  end
59
59
 
60
60
  shared_examples_for(
@@ -69,7 +69,7 @@ shared_examples_for(
69
69
  context "displays a list of ##{collection_css_id} in .#{member_css_class}" do
70
70
  subject { page }
71
71
  it do
72
- should(
72
+ expect(page).to(
73
73
  have_css(
74
74
  "##{collection_css_id} .#{member_css_class}",
75
75
  minimum: options[:minimum]
@@ -86,7 +86,7 @@ shared_examples_for(
86
86
  it_behaves_like 'a bootstrap page', title: title
87
87
  context "displays an item at ##{options[:css_id]}" do
88
88
  subject { page }
89
- it { should have_css("##{options[:css_id]}") }
89
+ it { is_expected.to have_css("##{options[:css_id]}") }
90
90
  end
91
91
  end
92
92
 
@@ -100,7 +100,7 @@ shared_examples_for 'a bootstrap form with errors' do |error_fields|
100
100
  )
101
101
  context 'shows which field has errors' do
102
102
  subject { page }
103
- it { should have_css('.form-group.has-error', text: field.to_s.humanize) }
103
+ it { is_expected.to have_css('.field_with_errors', text: field.to_s.humanize) }
104
104
  end
105
105
  end
106
106
  end
@@ -1,8 +1,12 @@
1
1
  shared_context 'attachments' do
2
2
  let(:image_attachment) do
3
- MessageTrain::Attachment.find_by_attachment_file_name('image-sample.jpg')
3
+ MessageTrain::Attachment
4
+ .joins(:attachment_blob)
5
+ .find_by(active_storage_blobs: { filename: 'image-sample.jpg' })
4
6
  end
5
7
  let(:pdf_attachment) do
6
- MessageTrain::Attachment.find_by_attachment_file_name('pdf-sample.pdf')
8
+ MessageTrain::Attachment
9
+ .joins(:attachment_blob)
10
+ .find_by(active_storage_blobs: { filename: 'pdf-sample.pdf' })
7
11
  end
8
12
  end
@@ -9,33 +9,62 @@ def show_page
9
9
  end
10
10
 
11
11
  def submit_via_button(button_name)
12
- button_id = find("input[type=submit][value='#{button_name}']")[:id]
13
- page.execute_script("$('##{button_id}').click();")
14
- end
15
-
16
- # Used to fill ckeditor fields
17
- # @param [String] locator label text for the textarea or textarea id
18
- def fill_in_ckeditor(locator, options)
19
- locator = find_field_by_label(locator)
20
- # Fill the editor content
21
- page.execute_script <<-SCRIPT
22
- var ckeditor = CKEDITOR.instances.#{locator}
23
- ckeditor.setData('#{ActionController::Base.helpers.j options[:with]}')
24
- ckeditor.focus()
25
- ckeditor.updateElement()
26
- SCRIPT
12
+ click_button button_name
27
13
  end
28
14
 
29
15
  def fill_in_autocomplete(field, options = {})
30
16
  field = find_field_by_label(field)
31
17
  fill_in field, with: options[:with]
32
18
 
33
- page.execute_script "$('##{field}').trigger('focus')"
34
- page.execute_script "$('##{field}').trigger('keydown')"
19
+ # The Stimulus controller debounces the search and populates a <datalist>
20
+ # with the matching participant slugs; resolve to the first suggestion
21
+ # rather than submitting the raw partial query as the recipient. Some
22
+ # callers intentionally use a query with no real match (e.g. to leave
23
+ # recipients unresolved for a draft), so don't wait/fail if none appear.
24
+ list_id = find_field(field)[:list]
25
+ return unless page.has_css?("##{list_id} option", visible: :all, wait: 1)
26
+
27
+ suggestion = find("##{list_id} option", visible: :all)
28
+ fill_in field, with: suggestion[:value]
29
+ end
30
+
31
+ # Cuprite drives the app on a real server thread with its own DB
32
+ # connection, so any marking it does through the browser commits outside
33
+ # the spec's transaction and would otherwise leak into later examples
34
+ # that reuse the same fixture conversation. Undo it the same way: on a
35
+ # separate thread, so this write also commits immediately instead of
36
+ # being rolled back with the rest of this example's (this thread's)
37
+ # transaction. SQLite only allows one writer at a time, and the RSpec
38
+ # thread's still-open transaction can be holding that lock at any given
39
+ # moment, so retry a few times rather than racing it.
40
+ def on_separate_connection(retries: 15)
41
+ Thread.new do
42
+ ActiveRecord::Base.connection_pool.with_connection do
43
+ begin
44
+ yield
45
+ rescue ActiveRecord::StatementInvalid => e
46
+ raise unless e.cause.is_a?(SQLite3::BusyException) && retries.positive?
47
+
48
+ sleep 0.3
49
+ retries -= 1
50
+ retry
51
+ end
52
+ end
53
+ end.join
54
+ end
35
55
 
36
- return unless page.has_selector?('.tt-menu .tt-suggestion')
37
- selector = ".tt-menu .tt-suggestion:contains('#{options[:with]}')"
38
- page.execute_script '$("' + selector + '").trigger("mouseenter").click()'
56
+ def undo_leaked_mark(mark_to_set, conversation: unread_conversation)
57
+ on_separate_connection do
58
+ box = first_user.box(:in)
59
+ # Ignore/unignore isn't a Receipt "marked_*" column like the other
60
+ # marks - it's tracked on its own model, routed through here rather
61
+ # than Box#mark (matching how BoxesController#destroy dispatches it).
62
+ if %i[ignore unignore].include?(mark_to_set)
63
+ MessageTrain::Ignore.send(mark_to_set, conversation, box)
64
+ else
65
+ box.mark(mark_to_set, conversations: conversation)
66
+ end
67
+ end
39
68
  end
40
69
 
41
70
  def find_field_by_label(locator)