ama_layout 6.3.0.pre → 6.10.0.pre
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.
- checksums.yaml +4 -4
- data/.travis.yml +2 -0
- data/ama_layout.gemspec +22 -20
- data/app/assets/javascripts/ama_layout/desktop/foundation-custom.js +10 -8
- data/app/assets/javascripts/ama_layout/desktop/index.js +1 -0
- data/app/assets/javascripts/ama_layout/notifications.coffee +17 -0
- data/app/controllers/ama_layout/api/v1/notifications_controller.rb +18 -0
- data/app/helpers/ama_layout_path_helper.rb +4 -4
- data/app/views/ama_layout/_notification.html.erb +10 -0
- data/app/views/ama_layout/_notification_sidebar.html.erb +22 -0
- data/app/views/ama_layout/_notifications.html.erb +6 -0
- data/app/views/ama_layout/_siteheader.html.erb +8 -3
- data/config/routes.rb +9 -0
- data/lib/ama_layout.rb +27 -20
- data/lib/ama_layout/decorators/navigation_decorator.rb +47 -0
- data/lib/ama_layout/decorators/notification_decorator.rb +45 -0
- data/lib/ama_layout/navigation.rb +3 -0
- data/lib/ama_layout/navigation.yml +58 -6
- data/lib/ama_layout/navigation_helper.rb +31 -0
- data/lib/ama_layout/notification.rb +89 -0
- data/lib/ama_layout/notification_scrubber.rb +13 -0
- data/lib/ama_layout/notification_set.rb +140 -0
- data/lib/ama_layout/notifications.rb +73 -0
- data/lib/ama_layout/notifications/abstract_store.rb +17 -0
- data/lib/ama_layout/notifications/redis_store.rb +38 -0
- data/lib/ama_layout/version.rb +1 -1
- data/spec/ama_layout/controllers/ama_layout/api/v1/notifications_controller_spec.rb +13 -0
- data/spec/ama_layout/decorators/navigation_decorator_spec.rb +121 -2
- data/spec/ama_layout/decorators/notification_decorator_spec.rb +57 -0
- data/spec/ama_layout/navigation_helper_spec.rb +63 -0
- data/spec/ama_layout/navigation_spec.rb +13 -43
- data/spec/ama_layout/notification_scrubber_spec.rb +10 -0
- data/spec/ama_layout/notification_set_spec.rb +281 -0
- data/spec/ama_layout/notification_spec.rb +193 -0
- data/spec/ama_layout/notifications/abstract_store_spec.rb +23 -0
- data/spec/ama_layout/notifications/redis_store_spec.rb +94 -0
- data/spec/ama_layout/notifications_spec.rb +109 -0
- data/spec/factories/users.rb +35 -0
- data/spec/helpers/ama_layout_path_helper_spec.rb +6 -6
- data/spec/internal/app/controllers/application_controller.rb +21 -0
- data/spec/internal/config/routes.rb +1 -0
- data/spec/spec_helper.rb +9 -14
- data/spec/support/shared_examples/member_navigation.rb +105 -0
- metadata +81 -18
- data/styles.scss +0 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
module AmaLayout
|
2
|
+
module NavigationHelper
|
3
|
+
def navigation
|
4
|
+
return AmaLayout::Navigation.non_member unless member?
|
5
|
+
case
|
6
|
+
when _has_outstanding_balance?
|
7
|
+
AmaLayout::Navigation.member_with_outstanding_balance
|
8
|
+
when _in_renewal_late?
|
9
|
+
AmaLayout::Navigation.member_in_renewal_late
|
10
|
+
when _in_renewal?
|
11
|
+
AmaLayout::Navigation.member_in_renewal
|
12
|
+
else
|
13
|
+
AmaLayout::Navigation.member
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def _in_renewal?
|
20
|
+
in_renewal
|
21
|
+
end
|
22
|
+
|
23
|
+
def _in_renewal_late?
|
24
|
+
status == "AL"
|
25
|
+
end
|
26
|
+
|
27
|
+
def _has_outstanding_balance?
|
28
|
+
has_outstanding_balance
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module AmaLayout
|
2
|
+
class Notification
|
3
|
+
include Draper::Decoratable
|
4
|
+
|
5
|
+
TYPES = %i[notice warning alert].freeze
|
6
|
+
DEFAULT_LIFESPAN = 1.year.freeze
|
7
|
+
FORMAT_VERSION = '1.0.0'.freeze
|
8
|
+
|
9
|
+
# NOTE: The following attributes are designed to be immutable - you need
|
10
|
+
# make a new instance to change them. The only mutable attribute is :active.
|
11
|
+
attr_reader :id, :type, :brand, :header, :content, :created_at, :lifespan,
|
12
|
+
:version
|
13
|
+
attr_accessor :active
|
14
|
+
|
15
|
+
def initialize(args = {})
|
16
|
+
args = args.with_indifferent_access
|
17
|
+
@id = args[:id]
|
18
|
+
@type = args.fetch(:type, :notice).to_sym
|
19
|
+
@brand = args[:brand]
|
20
|
+
@header = args.fetch(:header)
|
21
|
+
@content = args.fetch(:content)
|
22
|
+
@created_at = parse_time(args.fetch(:created_at))
|
23
|
+
@lifespan = parse_duration(args.fetch(:lifespan, DEFAULT_LIFESPAN))
|
24
|
+
@version = args.fetch(:version, FORMAT_VERSION)
|
25
|
+
self.active = args.fetch(:active)
|
26
|
+
invalid_type! if TYPES.exclude?(type)
|
27
|
+
end
|
28
|
+
|
29
|
+
def <=>(other)
|
30
|
+
created_at <=> other.created_at
|
31
|
+
end
|
32
|
+
|
33
|
+
def active?
|
34
|
+
active
|
35
|
+
end
|
36
|
+
|
37
|
+
def dismissed?
|
38
|
+
!active?
|
39
|
+
end
|
40
|
+
|
41
|
+
def dismiss!
|
42
|
+
self.active = false
|
43
|
+
dismissed?
|
44
|
+
end
|
45
|
+
|
46
|
+
def digest
|
47
|
+
Digest::SHA256.hexdigest(
|
48
|
+
"#{type}#{header}#{content}#{brand}#{version}"
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
def stale?
|
53
|
+
Time.current > created_at + lifespan
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_h
|
57
|
+
# NOTE: We want the following keys to be strings to provide
|
58
|
+
# consistency with the underlying data store.
|
59
|
+
{
|
60
|
+
'type' => type.to_s,
|
61
|
+
'brand' => brand,
|
62
|
+
'header' => header,
|
63
|
+
'content' => content,
|
64
|
+
'created_at' => created_at.iso8601,
|
65
|
+
'active' => active,
|
66
|
+
'lifespan' => lifespan.to_i,
|
67
|
+
'version' => version
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def invalid_type!
|
74
|
+
raise ArgumentError, "invalid notification type: #{type}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def parse_time(time)
|
78
|
+
time.is_a?(String) ? Time.zone.parse(time) : time
|
79
|
+
end
|
80
|
+
|
81
|
+
def parse_duration(duration)
|
82
|
+
if duration.is_a?(ActiveSupport::Duration)
|
83
|
+
duration
|
84
|
+
else
|
85
|
+
duration.seconds
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module AmaLayout
|
2
|
+
class NotificationScrubber < Rails::Html::PermitScrubber
|
3
|
+
def initialize
|
4
|
+
super
|
5
|
+
self.tags = %w(i a div span strong br em h1 h2 h3 h4 h5 h6 blockquote)
|
6
|
+
self.attributes = %w(href class id)
|
7
|
+
end
|
8
|
+
|
9
|
+
def skip_node?(node)
|
10
|
+
node.text?
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
module AmaLayout
|
2
|
+
# An array-like object that handles the storage and retrieval of notifications
|
3
|
+
# from the underlying data store.
|
4
|
+
#
|
5
|
+
# The raw serialization format is JSON as follows (keys are SHA256 hashes):
|
6
|
+
#
|
7
|
+
# {
|
8
|
+
# "57107043eab0f60a37f7735307dc6fc6709d04eec2dbeea8c284958057af9b77": {
|
9
|
+
# "type": "notice",
|
10
|
+
# "brand": "membership",
|
11
|
+
# "header": "test",
|
12
|
+
# "content": "test",
|
13
|
+
# "created_at": "2017-06-19T11:26:57.730-06:00",
|
14
|
+
# "lifespan": 31557600,
|
15
|
+
# "active": true,
|
16
|
+
# "version": "1.0.0"
|
17
|
+
# }
|
18
|
+
# }
|
19
|
+
#
|
20
|
+
class NotificationSet
|
21
|
+
include Enumerable
|
22
|
+
attr_accessor :base, :data_store, :key
|
23
|
+
|
24
|
+
delegate :each, :first, :last, :size, :[], :empty?, :any?, to: :all
|
25
|
+
|
26
|
+
def initialize(data_store, key)
|
27
|
+
self.data_store = data_store
|
28
|
+
self.key = key
|
29
|
+
self.base = fetch
|
30
|
+
clean!
|
31
|
+
end
|
32
|
+
|
33
|
+
def active
|
34
|
+
all.select(&:active?)
|
35
|
+
end
|
36
|
+
|
37
|
+
def all
|
38
|
+
@all ||= normalize(base_notifications)
|
39
|
+
end
|
40
|
+
|
41
|
+
def create(args = {})
|
42
|
+
args[:created_at] = Time.current
|
43
|
+
args[:active] = true
|
44
|
+
notification = Notification.new(args)
|
45
|
+
# previously dismissed notifications always take precendence
|
46
|
+
all.push(notification) unless base.key?(notification.digest)
|
47
|
+
save
|
48
|
+
end
|
49
|
+
|
50
|
+
def destroy!
|
51
|
+
data_store.delete(key) && reload!
|
52
|
+
end
|
53
|
+
|
54
|
+
def delete(*digests)
|
55
|
+
digests = Array.wrap(digests.flatten)
|
56
|
+
delta = all.reject { |n| digests.include?(n.digest) }
|
57
|
+
if delta != all
|
58
|
+
@all = delta
|
59
|
+
save
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def find(digest)
|
64
|
+
all.find { |n| n.id == digest }
|
65
|
+
end
|
66
|
+
|
67
|
+
def save
|
68
|
+
data_store.transaction do |store|
|
69
|
+
normalized = normalize(all)
|
70
|
+
self.base = serialize(normalized)
|
71
|
+
store.set(key, base.to_json)
|
72
|
+
end
|
73
|
+
reload!
|
74
|
+
end
|
75
|
+
|
76
|
+
def inspect
|
77
|
+
"<#{self.class.name}>: #{all}"
|
78
|
+
end
|
79
|
+
alias_method :to_s, :inspect
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def clean!
|
84
|
+
if dirty?
|
85
|
+
all.reject!(&:stale?)
|
86
|
+
save
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def dirty?
|
91
|
+
all.any?(&:stale?)
|
92
|
+
end
|
93
|
+
|
94
|
+
def reload!
|
95
|
+
@all = nil
|
96
|
+
self.base = fetch
|
97
|
+
all
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
def base_notifications
|
102
|
+
base.map { |k, v| Notification.new(v.merge(id: k)) }
|
103
|
+
end
|
104
|
+
|
105
|
+
def serialize(data)
|
106
|
+
data.inject({}) do |hash, element|
|
107
|
+
hash[element.digest] = element.to_h
|
108
|
+
hash
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def normalize(data)
|
113
|
+
# sort by reverse chronological order
|
114
|
+
data.sort { |a, b| b <=> a }
|
115
|
+
end
|
116
|
+
|
117
|
+
def fetch
|
118
|
+
result = data_store.get(key)
|
119
|
+
result.present? ? build(result) : {}
|
120
|
+
end
|
121
|
+
|
122
|
+
def build(raw)
|
123
|
+
JSON.parse(raw)
|
124
|
+
rescue JSON::ParserError
|
125
|
+
data_store.delete(key) # we should try to prevent further errors
|
126
|
+
::Rails.logger.error json_message(__FILE__, __LINE__, raw)
|
127
|
+
{}
|
128
|
+
end
|
129
|
+
|
130
|
+
def json_message(file, line, raw)
|
131
|
+
{
|
132
|
+
error: "#{self.class.name} - Invalid JSON",
|
133
|
+
file: file,
|
134
|
+
line: line,
|
135
|
+
key: key,
|
136
|
+
raw: raw
|
137
|
+
}.to_json
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module AmaLayout
|
2
|
+
# Usage:
|
3
|
+
#
|
4
|
+
# class MyClass
|
5
|
+
# include AmaLayout::Notifications
|
6
|
+
#
|
7
|
+
# notification_store AmaLayout::Notifications::RedisStore.new(options)
|
8
|
+
# notification_foreign_key :a_method_name_or_proc # defaults to :id
|
9
|
+
#
|
10
|
+
# ...
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
module Notifications
|
14
|
+
InvalidNotificationStore = Class.new(StandardError)
|
15
|
+
|
16
|
+
def self.included(base)
|
17
|
+
base.extend(ClassMethods)
|
18
|
+
base.include(InstanceMethods)
|
19
|
+
end
|
20
|
+
|
21
|
+
module InstanceMethods
|
22
|
+
def notifications
|
23
|
+
@notifications ||= NotificationSet.new(_store, _foreign_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
def notifications=(other)
|
27
|
+
@notifications = other
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def _store
|
33
|
+
self.class._notification_store || invalid_store!
|
34
|
+
end
|
35
|
+
|
36
|
+
def _foreign_key
|
37
|
+
self.class._notification_foreign_key.call(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
def invalid_store!
|
41
|
+
raise InvalidNotificationStore, 'a notification store must be specified'
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module ClassMethods
|
46
|
+
def notification_store(store)
|
47
|
+
self._notification_store = store
|
48
|
+
end
|
49
|
+
|
50
|
+
def notification_foreign_key(key)
|
51
|
+
self._notification_foreign_key = key
|
52
|
+
end
|
53
|
+
|
54
|
+
def _notification_foreign_key
|
55
|
+
@_notification_foreign_key || Proc.new(&:id)
|
56
|
+
end
|
57
|
+
|
58
|
+
def _notification_store
|
59
|
+
@_notification_store
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def _notification_store=(store)
|
65
|
+
@_notification_store = store
|
66
|
+
end
|
67
|
+
|
68
|
+
def _notification_foreign_key=(key)
|
69
|
+
@_notification_foreign_key = key.to_proc
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module AmaLayout
|
2
|
+
module Notifications
|
3
|
+
class AbstractStore
|
4
|
+
def get(key, opts = {})
|
5
|
+
raise NotImplementedError, 'you must define a #get method in a subclass'
|
6
|
+
end
|
7
|
+
|
8
|
+
def set(key, value, opts = {})
|
9
|
+
raise NotImplementedError, 'you must define a #set method in a subclass'
|
10
|
+
end
|
11
|
+
|
12
|
+
def delete(key, opts = {})
|
13
|
+
raise NotImplementedError, 'you must define a #delete method in a subclass'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module AmaLayout
|
2
|
+
module Notifications
|
3
|
+
class RedisStore < AbstractStore
|
4
|
+
delegate :clear, to: :base
|
5
|
+
|
6
|
+
attr_accessor :base
|
7
|
+
|
8
|
+
def initialize(opts = {})
|
9
|
+
self.base = ActiveSupport::Cache.lookup_store(
|
10
|
+
:redis_store,
|
11
|
+
opts.merge(raw: true)
|
12
|
+
)
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(key, opts = {})
|
16
|
+
if opts.fetch(:default, false)
|
17
|
+
base.fetch(key) { opts[:default] }
|
18
|
+
else
|
19
|
+
base.read(key)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def set(key, value, opts = {})
|
24
|
+
base.write(key, value, opts) == 'OK'
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(key, opts = {})
|
28
|
+
base.delete(key, opts) == 1
|
29
|
+
end
|
30
|
+
|
31
|
+
def transaction
|
32
|
+
base.data.multi do
|
33
|
+
yield self
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/ama_layout/version.rb
CHANGED
@@ -0,0 +1,13 @@
|
|
1
|
+
describe AmaLayout::Api::V1::NotificationsController, type: :controller do
|
2
|
+
describe 'DELETE api/v1/notifications' do
|
3
|
+
routes { AmaLayout::Engine.routes }
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
delete :dismiss_all
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'returns a 204 No Content status' do
|
10
|
+
expect(response).to have_http_status(:no_content)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -7,8 +7,8 @@ describe AmaLayout::NavigationDecorator do
|
|
7
7
|
let(:membership_site) { "http://membership.waffles.ca" }
|
8
8
|
let(:driveredonline_site) { "http://driveredonline.waffles.ca" }
|
9
9
|
let(:registries_site) { "http://registries.waffles.ca" }
|
10
|
-
let(:automotive_site) { "http://automotive.waffles.ca"}
|
11
|
-
let(:travel_site) { "http://travel.waffles.ca"}
|
10
|
+
let(:automotive_site) { "http://automotive.waffles.ca" }
|
11
|
+
let(:travel_site) { "http://travel.waffles.ca" }
|
12
12
|
|
13
13
|
before(:each) do
|
14
14
|
allow(Rails.configuration).to receive(:gatekeeper_site).and_return(gatekeeper_site)
|
@@ -129,4 +129,123 @@ describe AmaLayout::NavigationDecorator do
|
|
129
129
|
expect(navigation_presenter.account_toggle).to eq "render"
|
130
130
|
end
|
131
131
|
end
|
132
|
+
|
133
|
+
context 'notification center' do
|
134
|
+
let(:store) do
|
135
|
+
AmaLayout::Notifications::RedisStore.new(
|
136
|
+
db: 4,
|
137
|
+
namespace: 'test_notifications',
|
138
|
+
host: 'localhost'
|
139
|
+
)
|
140
|
+
end
|
141
|
+
let(:notification_set) { AmaLayout::NotificationSet.new(store, 1) }
|
142
|
+
let(:user) { OpenStruct.new(navigation: 'member', notifications: notification_set) }
|
143
|
+
let(:navigation) { FactoryGirl.build :navigation, user: user }
|
144
|
+
subject { described_class.new(navigation) }
|
145
|
+
|
146
|
+
around(:each) do |example|
|
147
|
+
Timecop.freeze(Time.zone.local(2017, 6, 19)) do
|
148
|
+
store.clear
|
149
|
+
example.run
|
150
|
+
store.clear
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe '#notifications' do
|
155
|
+
it 'renders the content to the page' do
|
156
|
+
expect(subject.h).to receive(:render).once.and_return true
|
157
|
+
expect(subject.notifications).to be true
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe '#notification_badge' do
|
162
|
+
context 'with 1 active notification' do
|
163
|
+
before(:each) do
|
164
|
+
user.notifications.create(
|
165
|
+
type: :warning,
|
166
|
+
header: 'test',
|
167
|
+
content: 'test'
|
168
|
+
)
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'returns a div with the count of active notifications' do
|
172
|
+
expect(subject.notification_badge).to include('div')
|
173
|
+
expect(subject.notification_badge).to include('1')
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context 'with only inactive notifications' do
|
178
|
+
before(:each) do
|
179
|
+
user.notifications.create(
|
180
|
+
type: :warning,
|
181
|
+
header: 'test',
|
182
|
+
content: 'test'
|
183
|
+
)
|
184
|
+
user.notifications.first.dismiss!
|
185
|
+
user.notifications.save
|
186
|
+
end
|
187
|
+
|
188
|
+
it 'does not return the badge markup' do
|
189
|
+
expect(subject.notification_badge).to be nil
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context 'with only active and inactive notifications' do
|
194
|
+
before(:each) do
|
195
|
+
user.notifications.create(
|
196
|
+
type: :warning,
|
197
|
+
header: 'test',
|
198
|
+
content: 'test'
|
199
|
+
)
|
200
|
+
2.times do |i|
|
201
|
+
user.notifications.create(
|
202
|
+
type: :notice,
|
203
|
+
header: i,
|
204
|
+
content: i
|
205
|
+
)
|
206
|
+
end
|
207
|
+
user.notifications.first.dismiss!
|
208
|
+
user.notifications.save
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'returns a div with the count of active notifications' do
|
212
|
+
expect(subject.notification_badge).to include('div')
|
213
|
+
expect(subject.notification_badge).to include('2')
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
describe '#notification_sidebar' do
|
219
|
+
it 'renders content to the page' do
|
220
|
+
expect(subject.h).to receive(:render).once.and_return true
|
221
|
+
expect(subject.notification_sidebar).to be true
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
describe '#notifications_heading' do
|
226
|
+
context 'when notifications are present' do
|
227
|
+
before(:each) do
|
228
|
+
user.notifications.create(
|
229
|
+
type: :warning,
|
230
|
+
header: 'test',
|
231
|
+
content: 'test'
|
232
|
+
)
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'returns the correct heading' do
|
236
|
+
expect(subject.notifications_heading).to include('Most Recent Notifications')
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
context 'when notifications are not present' do
|
241
|
+
it 'returns the correct heading' do
|
242
|
+
expect(subject.notifications_heading).to include('No Recent Notifications')
|
243
|
+
end
|
244
|
+
|
245
|
+
it 'italicizes the message' do
|
246
|
+
expect(subject.notifications_heading).to include('italic')
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
132
251
|
end
|