marty 8.0.0 → 8.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/.gitlab-ci.yml +7 -0
  3. data/app/assets/javascripts/marty/cable.js +12 -0
  4. data/app/assets/stylesheets/marty/codemirror/notifications.css +4 -0
  5. data/app/channels/application_cable/channel.rb +4 -0
  6. data/app/channels/application_cable/connection.rb +17 -0
  7. data/app/channels/marty/notification_channel.rb +8 -0
  8. data/app/components/marty/auth_app.rb +54 -6
  9. data/app/components/marty/auth_app/client/auth_app.js +53 -0
  10. data/app/components/marty/main_auth_app.rb +47 -1
  11. data/app/components/marty/notifications/config_view.rb +56 -0
  12. data/app/components/marty/notifications/deliveries_view.rb +50 -0
  13. data/app/components/marty/notifications/grid_view.rb +56 -0
  14. data/app/components/marty/notifications/window.rb +23 -0
  15. data/app/components/marty/promise_view.rb +13 -1
  16. data/app/components/marty/promise_view/client/promise_view.js +18 -0
  17. data/app/components/marty/{schedule_jobs_dashboard/client/schedule_jobs_dashboard.js → schedule_jobs_grid/client/schedule_jobs_grid.js} +0 -0
  18. data/app/components/marty/simple_app/client/simple_app.js +5 -1
  19. data/app/components/marty/users/user_view.rb +177 -0
  20. data/app/controllers/marty/application_controller.rb +13 -0
  21. data/app/models/marty/data_grid.rb +1 -1
  22. data/app/models/marty/notifications.rb +4 -0
  23. data/app/models/marty/notifications/config.rb +25 -0
  24. data/app/models/marty/notifications/delivery.rb +56 -0
  25. data/app/models/marty/notifications/event_type.rb +11 -0
  26. data/app/models/marty/notifications/notification.rb +25 -0
  27. data/app/models/marty/promise.rb +1 -0
  28. data/app/models/marty/user.rb +14 -0
  29. data/app/services/marty/notifications/create.rb +39 -0
  30. data/app/services/marty/notifications/create_deliveries.rb +25 -0
  31. data/app/services/marty/notifications/process_delivery.rb +11 -0
  32. data/app/services/marty/notifications/processors/email.rb +13 -0
  33. data/app/services/marty/notifications/processors/sms.rb +13 -0
  34. data/app/services/marty/notifications/processors/web.rb +27 -0
  35. data/app/services/marty/promises/cancel.rb +37 -0
  36. data/app/services/marty/promises/delorean/create.rb +4 -0
  37. data/app/services/marty/promises/ruby/create.rb +4 -1
  38. data/app/views/marty/diagnostic/op.html.erb +4 -0
  39. data/db/migrate/514_remove_marty_events.rb +1 -1
  40. data/db/migrate/519_create_marty_notifications_event_types.rb +16 -0
  41. data/db/migrate/520_create_marty_notifications.rb +11 -0
  42. data/db/migrate/521_create_marty_notifications_deliveries.rb +22 -0
  43. data/db/migrate/522_create_marty_notifications_config.rb +21 -0
  44. data/db/sql/lookup_grid_distinct_v1.sql +6 -6
  45. data/lib/marty.rb +2 -0
  46. data/lib/marty/diagnostic/version.rb +36 -26
  47. data/lib/marty/engine.rb +7 -1
  48. data/lib/marty/mcfly_model.rb +27 -6
  49. data/lib/marty/railtie.rb +1 -0
  50. data/lib/marty/version.rb +1 -1
  51. data/marty.gemspec +3 -0
  52. data/spec/controllers/diagnostic/controller_spec.rb +1 -1
  53. data/spec/dummy/app/models/gemini/fannie_bup.rb +27 -13
  54. data/spec/dummy/app/models/gemini/helper.rb +62 -0
  55. data/spec/features/notifications_spec.rb +224 -0
  56. data/spec/job_helper.rb +14 -1
  57. data/spec/lib/mcfly_model_spec.rb +14 -7
  58. data/spec/models/promise_spec.rb +121 -0
  59. data/spec/services/notifications/create_spec.rb +82 -0
  60. metadata +73 -3
@@ -109,7 +109,19 @@ LOGGER:
109
109
  result = Gemini::Helper.testlog('message', [msgid]) &&
110
110
  Gemini::Helper.testaction('message %d', msgid) && msgid
111
111
  EOS
112
-
112
+ NAME_L = 'PromiseL'
113
+ SCRIPT_L = <<EOS
114
+ Node:
115
+ jobs = [1, 2, 3]
116
+ job_title =? 'base'
117
+ base_attr = [ (Node(p_title = ('%s %s' % [job_title, j]),
118
+ job_title = ('%s %s' % [job_title, j])) | "run0") == true
119
+ for j in jobs ]
120
+ run0 = [ (Node(p_title = ('%s %s' % [job_title, j]),
121
+ job_title = ('%s %s' % [job_title, j])) | "run1") == true
122
+ for j in jobs ]
123
+ run1 = Gemini::Helper.sleep(5, job_title)
124
+ EOS
113
125
  NAME_M = 'PromiseM'
114
126
  SCRIPT_M = <<EOS
115
127
  Node:
@@ -160,6 +172,7 @@ def promise_bodies
160
172
  NAME_I => SCRIPT_I,
161
173
  NAME_J => SCRIPT_J,
162
174
  NAME_K => SCRIPT_K,
175
+ NAME_L => SCRIPT_L,
163
176
  NAME_M => SCRIPT_M,
164
177
  NAME_N => SCRIPT_N,
165
178
  }
@@ -46,6 +46,7 @@ A:
46
46
 
47
47
  a_func = Gemini::FannieBup.a_func('infinity', e_id, bc_id)
48
48
  b_func = Gemini::FannieBup.b_func('infinity', e_id, bc_id, 12)
49
+ c_func = Gemini::FannieBup.c_func('infinity', e_id, bc_id, 12)
49
50
  EOF
50
51
  errscript = <<EOF
51
52
  Err:
@@ -121,22 +122,23 @@ describe 'McflyModel' do
121
122
  it 'lookup mode default' do
122
123
  a1 = @engine.evaluate('A', 'lookup', params)
123
124
  a2 = @engine.evaluate('A', 'clookup', params)
124
- expect(a1).to eq(a2) # cache/non return same
125
- expect(a1.class).to eq(OpenStruct) # mode default so return OS
126
- expect(a2.class).to eq(OpenStruct)
125
+ expect(a1).to eq(a2) # cache/non return same
126
+ expect(a1.class).to eq(Hash) # mode default so return hash
127
+ expect(a2.class).to eq(Hash)
127
128
 
128
129
  # check that keys are non mcfly non uniqueness
129
- expect(a1.to_h.keys.to_set).to eq(Set[:buy_up, :buy_down])
130
+ expect(a1.to_h.keys.to_set).to eq(Set['buy_up', 'buy_down'])
130
131
  end
131
132
 
132
133
  it 'lookup non generated' do
133
134
  # a1 will be AR Relations
134
- # b1 will be OpenStructs because the b fns return #first
135
+ # b1 will be hash because the b fns return #first
135
136
  e_id = Gemini::Entity.where(name: 'PLS').first.id
136
137
  bc_id = Gemini::BudCategory.where(name: 'Conv Fixed 20').first.id
137
138
  p = { 'e_id' => e_id, 'bc_id' => bc_id }
138
139
  a1 = @engine.evaluate('A', 'a_func', p)
139
140
  b1 = @engine.evaluate('A', 'b_func', p)
141
+ c1 = @engine.evaluate('A', 'c_func', p)
140
142
 
141
143
  # all return relations
142
144
  expect(ActiveRecord::Relation === a1).to be_truthy
@@ -150,10 +152,15 @@ describe 'McflyModel' do
150
152
  # a1 is AR but still missing the FK entity_id so will raise
151
153
  expect { a1.first.entity }.to raise_error(/missing attribute: entity_id/)
152
154
 
153
- expect(b1.class).to eq(OpenStruct)
155
+ expect(b1.class).to eq(Hash)
154
156
 
155
157
  # make sure b1 has correct keys
156
- expect(b1.to_h.keys.to_set).to eq(Set[:buy_up, :buy_down])
158
+ expect(b1.to_h.keys.to_set).to eq(Set['buy_up', 'buy_down'])
159
+
160
+ expect(c1.class).to eq(OpenStruct)
161
+
162
+ # make sure c1 has correct keys
163
+ expect(c1.to_h.keys.to_set).to eq(Set[:buy_up, :buy_down])
157
164
  end
158
165
 
159
166
  it 'lookup mode nil' do
@@ -93,6 +93,127 @@ describe Marty::Promise, slow: true, retry: 3 do
93
93
  expect(log.message).to eq 'was called'
94
94
  end
95
95
 
96
+ it 'can cancel jobs' do
97
+ run_ruby_job = lambda do |title1, title2|
98
+ title = title1 + ' ' + title2
99
+ Marty::Promises::Ruby::Create.call(
100
+ module_name: 'Gemini::Helper',
101
+ method_name: 'promise_test',
102
+ method_args: [title2],
103
+ params: {
104
+ p_title: title,
105
+ _user_id: 1,
106
+ }
107
+ )
108
+ end
109
+ engine = Marty::ScriptSet.new.get_engine(NAME_L)
110
+ run_delorean_job = lambda do |title1, title2|
111
+ engine.background_eval('Node', { 'p_title' => title1 + ' ' + title2,
112
+ 'job_title' => title2 }, ['base_attr'])
113
+ end
114
+
115
+ aggregate_failures do
116
+ [['Ruby', run_ruby_job],
117
+ ['PromiseL', run_delorean_job]].each do |title1, runner|
118
+ # first run with no cancel. make sure the test parts work as expected
119
+ title2 = 'first run'
120
+ x = runner.call(title1, title2)
121
+
122
+ base_p = nil
123
+ timeout = 60
124
+ # wait until base promise completes
125
+ loop do
126
+ base_p = Marty::Promise.find_by("title like '#{title1}%'")
127
+ break if p&.status || timeout == 0
128
+
129
+ timeout -= 1
130
+ sleep 1
131
+ end
132
+ expect(base_p.is_a?(Marty::Promise)).to be_truthy
133
+ expect(base_p.status).to be_truthy
134
+ expect(timeout).to be < 55
135
+
136
+ # count promises that ran
137
+ ps = Marty::Promise.where("title like '#{title2} %' or "\
138
+ "title = '#{title1} #{title2}'").
139
+ pluck(:id, :title, :end_dt, :status, :result)
140
+
141
+ # this check could fail on rare occasion due to the fact that
142
+ # delayed jobs sometimes run twice
143
+ expect(ps.count).to eq(13)
144
+
145
+ # check status
146
+ expect(ps.all? { |p| p[4] }).to be_truthy
147
+
148
+ # check the names
149
+ exp_pnames = [
150
+ 'first run 1', 'first run 1 1', 'first run 1 2', 'first run 1 3',
151
+ 'first run 2', 'first run 2 1', 'first run 2 2', 'first run 2 3',
152
+ 'first run 3', 'first run 3 1', 'first run 3 2', 'first run 3 3'
153
+ ]
154
+
155
+ expect(ps.map { |p| p[1] }.reject { |s| s.starts_with?(title1) }.sort).
156
+ to eq(exp_pnames)
157
+
158
+ # make sure the log was written by the leaf jobs. (uniq because
159
+ # rarely jobs run twice due to race condition -- see promise.rb:126)
160
+ exp_log = ['first run 1 1', 'first run 1 2', 'first run 1 3',
161
+ 'first run 2 1', 'first run 2 2', 'first run 2 3',
162
+ 'first run 3 1', 'first run 3 2', 'first run 3 3']
163
+ logs = Marty::Log.all.pluck(:details).map { |d| d['label'] }.sort.uniq
164
+ expect(logs).to eq(exp_log)
165
+
166
+ Marty::Promise.where("title like '#{title2}%'").destroy_all
167
+ Marty::Log.where("details->>'label' like '#{title2}%'").destroy_all
168
+
169
+ # run with early cancel
170
+ cancel_with_checks(runner, title1, '2nd run', '1', ps.count,
171
+ exp_log.count)
172
+
173
+ # run with later cancel
174
+ cancel_with_checks(runner, title1, '3rd run', '1 1', ps.count,
175
+ exp_log.count)
176
+
177
+ # some workers may die because we deleted the promises,
178
+ # so restart them
179
+ stop_delayed_job
180
+ start_delayed_job
181
+ end
182
+ end
183
+ end
184
+
185
+ def cancel_with_checks(runner, title1, title2, cancel_name, cnt1, cnt2)
186
+ testinfo = "#{title1} #{title2}"
187
+ runner.call(title1, title2)
188
+
189
+ # wait for indicated job and cancel
190
+ timeout = 30
191
+ title_where = "title = '#{title2} #{cancel_name}'"
192
+ until timeout == 0 || (p = Marty::Promise.find_by(title_where))
193
+ sleep 1
194
+ timeout -= 1
195
+ end
196
+ expect(p).to be_a(Marty::Promise), testinfo
197
+ Marty::Promises::Cancel.call(p.id)
198
+
199
+ # count the logs that were generated, should be less
200
+ l = Marty::Log.where("details->>'label' like '#{title2}%'").uniq
201
+ expect(l.count).to be < cnt2, testinfo
202
+
203
+ ps = Marty::Promise.where("title like '#{title2}%' or "\
204
+ "title = '#{title1} #{title2}'").
205
+ pluck(:job_id, :id, :title, :end_dt, :status, :result)
206
+
207
+ # cancel should have stopped creation of promises
208
+ expect(ps.count).to be < cnt1, testinfo
209
+
210
+ # make sure all the promises have an error=Cancelled in result
211
+ errors = ps.map(&:last).map { |h| h['error'] }.to_set
212
+ expect(errors).to eq(['Cancelled'].to_set), testinfo
213
+ Marty::Promise.where("title like '#{title2}%'").destroy_all
214
+ Marty::Log.where("details->>'label' like '#{title2}%'").destroy_all
215
+ end
216
+
96
217
  it 'fails on exception' do
97
218
  expect(Marty::Promise.where(title: 'PromiseJ').exists?).to be false
98
219
 
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ module Marty
4
+ describe Notifications::Create do
5
+ before do
6
+ populate_test_users
7
+ end
8
+
9
+ let(:user1) do
10
+ Marty::User.find_by(login: :admin1)
11
+ end
12
+
13
+ let(:user2) do
14
+ Marty::User.find_by(login: :admin2)
15
+ end
16
+
17
+ let(:event_type) do
18
+ 'API reached the limit'
19
+ end
20
+
21
+ let!(:config) do
22
+ ::Marty::Notifications::Config.create!(
23
+ event_type: event_type,
24
+ recipient: user1,
25
+ delivery_type: :web,
26
+ state: :on,
27
+ text: 'Please contact the customer'
28
+ )
29
+ end
30
+
31
+ let!(:config2) do
32
+ ::Marty::Notifications::Config.create!(
33
+ event_type: event_type,
34
+ recipient: user2,
35
+ delivery_type: :web,
36
+ state: :on,
37
+ )
38
+ end
39
+
40
+ def create_notification
41
+ hash = described_class.call(
42
+ event_type: event_type,
43
+ text: 'Test limit is 1000'
44
+ )
45
+
46
+ Marty::Notifications::Notification.find(hash[:id])
47
+ end
48
+
49
+ it 'creates deliveries' do
50
+ notification = create_notification
51
+
52
+ expect(notification.deliveries.size).to eq 2
53
+
54
+ delivery1 = user1.notification_deliveries.find_by(
55
+ notification: notification
56
+ )
57
+
58
+ expect(delivery1.state).to eq 'sent'
59
+ expect(delivery1.text).to eq 'Please contact the customer'
60
+
61
+ delivery2 = user2.notification_deliveries.find_by(
62
+ notification: notification
63
+ )
64
+ expect(delivery2.state).to eq 'sent'
65
+ expect(delivery2.text).to eq ''
66
+ end
67
+
68
+ it "doesn't create delivery when config is off" do
69
+ config2.update!(state: :off)
70
+
71
+ notification = create_notification
72
+
73
+ expect(notification.deliveries.size).to eq 1
74
+
75
+ delivery2 = user2.notification_deliveries.find_by(
76
+ notification: notification
77
+ )
78
+
79
+ expect(delivery2).to_not be_present
80
+ end
81
+ end
82
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: marty
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.0
4
+ version: 8.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arman Bostani
@@ -14,8 +14,22 @@ authors:
14
14
  autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
- date: 2019-11-08 00:00:00.000000000 Z
17
+ date: 2019-11-26 00:00:00.000000000 Z
18
18
  dependencies:
19
+ - !ruby/object:Gem::Dependency
20
+ name: actioncable
21
+ requirement: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ type: :runtime
27
+ prerelease: false
28
+ version_requirements: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
19
33
  - !ruby/object:Gem::Dependency
20
34
  name: aws-sigv4
21
35
  requirement: !ruby/object:Gem::Requirement
@@ -212,6 +226,34 @@ dependencies:
212
226
  - - ">="
213
227
  - !ruby/object:Gem::Version
214
228
  version: '0'
229
+ - !ruby/object:Gem::Dependency
230
+ name: state_machines
231
+ requirement: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0'
236
+ type: :runtime
237
+ prerelease: false
238
+ version_requirements: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: '0'
243
+ - !ruby/object:Gem::Dependency
244
+ name: state_machines-activerecord
245
+ requirement: !ruby/object:Gem::Requirement
246
+ requirements:
247
+ - - ">="
248
+ - !ruby/object:Gem::Version
249
+ version: '0'
250
+ type: :runtime
251
+ prerelease: false
252
+ version_requirements: !ruby/object:Gem::Requirement
253
+ requirements:
254
+ - - ">="
255
+ - !ruby/object:Gem::Version
256
+ version: '0'
215
257
  - !ruby/object:Gem::Dependency
216
258
  name: zip-zip
217
259
  requirement: !ruby/object:Gem::Requirement
@@ -248,6 +290,7 @@ files:
248
290
  - README.md
249
291
  - Rakefile
250
292
  - app/assets/javascripts/marty/application.js
293
+ - app/assets/javascripts/marty/cable.js
251
294
  - app/assets/javascripts/marty/codemirror/codemirror.js
252
295
  - app/assets/javascripts/marty/codemirror/mode/delorean/delorean.js
253
296
  - app/assets/javascripts/marty/codemirror/mode/javascript/javascript.js
@@ -257,7 +300,11 @@ files:
257
300
  - app/assets/stylesheets/marty/application.css
258
301
  - app/assets/stylesheets/marty/codemirror/codemirror.css
259
302
  - app/assets/stylesheets/marty/codemirror/delorean.css
303
+ - app/assets/stylesheets/marty/codemirror/notifications.css
260
304
  - app/assets/stylesheets/marty/dark_mode.css
305
+ - app/channels/application_cable/channel.rb
306
+ - app/channels/application_cable/connection.rb
307
+ - app/channels/marty/notification_channel.rb
261
308
  - app/components/marty/api_auth_view.rb
262
309
  - app/components/marty/api_config_view.rb
263
310
  - app/components/marty/api_log_view.rb
@@ -290,6 +337,10 @@ files:
290
337
  - app/components/marty/new_posting_form.rb
291
338
  - app/components/marty/new_posting_form/client/new_posting_form.js
292
339
  - app/components/marty/new_posting_window.rb
340
+ - app/components/marty/notifications/config_view.rb
341
+ - app/components/marty/notifications/deliveries_view.rb
342
+ - app/components/marty/notifications/grid_view.rb
343
+ - app/components/marty/notifications/window.rb
293
344
  - app/components/marty/panel.rb
294
345
  - app/components/marty/panel/client/panel.js
295
346
  - app/components/marty/posting_grid.rb
@@ -306,8 +357,8 @@ files:
306
357
  - app/components/marty/reporting.rb
307
358
  - app/components/marty/reporting/client/reporting.js
308
359
  - app/components/marty/schedule_jobs_dashboard.rb
309
- - app/components/marty/schedule_jobs_dashboard/client/schedule_jobs_dashboard.js
310
360
  - app/components/marty/schedule_jobs_grid.rb
361
+ - app/components/marty/schedule_jobs_grid/client/schedule_jobs_grid.js
311
362
  - app/components/marty/schedule_jobs_logs.rb
312
363
  - app/components/marty/schedule_jobs_logs/client/schedule_jobs_logs.js
313
364
  - app/components/marty/script_form.rb
@@ -322,6 +373,7 @@ files:
322
373
  - app/components/marty/simple_app/client/statusbar_ext.js
323
374
  - app/components/marty/tag_grid.rb
324
375
  - app/components/marty/user_view.rb
376
+ - app/components/marty/users/user_view.rb
325
377
  - app/controllers/marty/application_controller.rb
326
378
  - app/controllers/marty/components_controller.rb
327
379
  - app/controllers/marty/delayed_job_controller.rb
@@ -356,6 +408,11 @@ files:
356
408
  - app/models/marty/import_type.rb
357
409
  - app/models/marty/log.rb
358
410
  - app/models/marty/name_validator.rb
411
+ - app/models/marty/notifications.rb
412
+ - app/models/marty/notifications/config.rb
413
+ - app/models/marty/notifications/delivery.rb
414
+ - app/models/marty/notifications/event_type.rb
415
+ - app/models/marty/notifications/notification.rb
359
416
  - app/models/marty/pg_enum.rb
360
417
  - app/models/marty/posting.rb
361
418
  - app/models/marty/posting_type.rb
@@ -373,6 +430,13 @@ files:
373
430
  - app/services/marty/data_grid_view/save_grid.rb
374
431
  - app/services/marty/enums/report.rb
375
432
  - app/services/marty/jobs/schedule.rb
433
+ - app/services/marty/notifications/create.rb
434
+ - app/services/marty/notifications/create_deliveries.rb
435
+ - app/services/marty/notifications/process_delivery.rb
436
+ - app/services/marty/notifications/processors/email.rb
437
+ - app/services/marty/notifications/processors/sms.rb
438
+ - app/services/marty/notifications/processors/web.rb
439
+ - app/services/marty/promises/cancel.rb
376
440
  - app/services/marty/promises/delorean.rb
377
441
  - app/services/marty/promises/delorean/create.rb
378
442
  - app/services/marty/promises/ruby.rb
@@ -437,6 +501,10 @@ files:
437
501
  - db/migrate/512_add_promise_priority.rb
438
502
  - db/migrate/513_add_priority_to_promise_view.rb
439
503
  - db/migrate/514_remove_marty_events.rb
504
+ - db/migrate/519_create_marty_notifications_event_types.rb
505
+ - db/migrate/520_create_marty_notifications.rb
506
+ - db/migrate/521_create_marty_notifications_deliveries.rb
507
+ - db/migrate/522_create_marty_notifications_config.rb
440
508
  - db/seeds.rb
441
509
  - db/sql/lookup_grid_distinct_v1.sql
442
510
  - db/sql/query_grid_dir_v1.sql
@@ -1636,6 +1704,7 @@ files:
1636
1704
  - spec/features/javascripts/login.js.coffee
1637
1705
  - spec/features/jobs_dashboard_spec.rb
1638
1706
  - spec/features/log_view_spec.rb
1707
+ - spec/features/notifications_spec.rb
1639
1708
  - spec/features/reporting_spec.rb
1640
1709
  - spec/features/rule_spec.rb
1641
1710
  - spec/features/schedule_jobs_dashboard_spec.rb
@@ -1700,6 +1769,7 @@ files:
1700
1769
  - spec/requests/routes_spec.rb
1701
1770
  - spec/services/background_job/fetch_missing_in_schedule_cron_jobs_spec.rb
1702
1771
  - spec/services/jobs/schedule_spec.rb
1772
+ - spec/services/notifications/create_spec.rb
1703
1773
  - spec/spec_helper.rb
1704
1774
  - spec/support/chromedriver.rb
1705
1775
  - spec/support/components/netzke_combobox.rb