howitzer 2.0.0 → 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +21 -1
  4. data/features/cli_new.feature +1 -0
  5. data/features/cli_update.feature +1 -0
  6. data/generators/base_generator.rb +3 -1
  7. data/generators/config/templates/capybara.rb +1 -1
  8. data/generators/config/templates/default.yml +4 -2
  9. data/generators/cucumber/cucumber_generator.rb +1 -0
  10. data/generators/cucumber/templates/common_steps.rb +1 -1
  11. data/generators/cucumber/templates/env.rb +0 -28
  12. data/generators/cucumber/templates/hooks.rb +29 -0
  13. data/generators/prerequisites/templates/user.rb +2 -0
  14. data/generators/prerequisites/templates/users.rb +2 -2
  15. data/generators/root/templates/Gemfile.erb +1 -1
  16. data/generators/rspec/templates/spec_helper.rb +9 -2
  17. data/generators/turnip/templates/spec_helper.rb +9 -2
  18. data/generators/web/templates/example_page.rb +0 -1
  19. data/howitzer.gemspec +1 -3
  20. data/lib/howitzer.rb +7 -0
  21. data/lib/howitzer/cache.rb +1 -1
  22. data/lib/howitzer/capybara_helpers.rb +2 -2
  23. data/lib/howitzer/email.rb +26 -8
  24. data/lib/howitzer/exceptions.rb +19 -19
  25. data/lib/howitzer/mail_adapters/abstract.rb +4 -3
  26. data/lib/howitzer/mail_adapters/mailgun.rb +6 -5
  27. data/lib/howitzer/mailgun_api/client.rb +1 -1
  28. data/lib/howitzer/version.rb +1 -1
  29. data/lib/howitzer/web/base_section.rb +3 -3
  30. data/lib/howitzer/web/capybara_context_holder.rb +19 -0
  31. data/lib/howitzer/web/capybara_methods_proxy.rb +27 -19
  32. data/lib/howitzer/web/element_dsl.rb +52 -13
  33. data/lib/howitzer/web/iframe_dsl.rb +3 -7
  34. data/lib/howitzer/web/page.rb +14 -21
  35. data/lib/howitzer/web/page_dsl.rb +32 -4
  36. data/lib/howitzer/web/page_validator.rb +15 -16
  37. data/lib/howitzer/web/section_dsl.rb +3 -7
  38. data/spec/config/custom.yml +1 -1
  39. data/spec/spec_helper.rb +1 -7
  40. data/spec/support/shared_examples/capybara_context_holder.rb +3 -3
  41. data/spec/support/shared_examples/element_dsl.rb +128 -18
  42. data/spec/unit/generators/base_generator_spec.rb +15 -16
  43. data/spec/unit/generators/cucumber_generator_spec.rb +2 -0
  44. data/spec/unit/generators/root_generator_spec.rb +1 -1
  45. data/spec/unit/lib/capybara_helpers_spec.rb +2 -2
  46. data/spec/unit/lib/email_spec.rb +37 -6
  47. data/spec/unit/lib/{howitzer.rb → howitzer_spec.rb} +9 -0
  48. data/spec/unit/lib/mail_adapters/abstract_spec.rb +1 -1
  49. data/spec/unit/lib/mail_adapters/mailgun_spec.rb +4 -4
  50. data/spec/unit/lib/web/base_section_spec.rb +3 -1
  51. data/spec/unit/lib/web/element_dsl_spec.rb +7 -2
  52. data/spec/unit/lib/web/page_dsl_spec.rb +22 -0
  53. data/spec/unit/lib/web/page_spec.rb +79 -44
  54. data/spec/unit/lib/web/page_validator_spec.rb +94 -51
  55. data/spec/unit/lib/web/section_spec.rb +4 -2
  56. metadata +10 -8
@@ -16,13 +16,6 @@ module Howitzer
16
16
  @validations ||= {}
17
17
  end
18
18
 
19
- # Returns page list
20
- # @return [Array]
21
-
22
- def self.pages
23
- @pages ||= []
24
- end
25
-
26
19
  # Checks if any validations are defined for the page
27
20
  # @raise [Howitzer::NoValidationError] if no one validation is defined for the page
28
21
 
@@ -60,13 +53,13 @@ module Howitzer
60
53
  end
61
54
 
62
55
  # Check whether current page is opened or no
56
+ # @param sync [Boolean] if true then waits until validation true during Howitzer.capybara_wait_time
57
+ # or returns false. If false, returns result immediately
63
58
  # @return [Boolean]
64
59
  # @raise [Howitzer::NoValidationError] if no one validation is defined for the page
65
60
 
66
- def opened?
67
- if validations.present?
68
- return !validations.any? { |(_, validation)| !validation.call(self) }
69
- end
61
+ def opened?(sync: true)
62
+ return validations.all? { |(_, validation)| validation.call(self, sync) } if validations.present?
70
63
  raise Howitzer::NoValidationError, "No any page validation was found for '#{name}' page"
71
64
  end
72
65
 
@@ -74,30 +67,36 @@ module Howitzer
74
67
  # @return [Array] page name list
75
68
 
76
69
  def matched_pages
77
- PageValidator.pages.select(&:opened?)
70
+ PageValidator.validations.keys.select { |klass| klass.opened?(sync: false) }
78
71
  end
79
72
 
80
73
  # @return [Hash] defined validations for current page class
81
74
 
82
75
  def validations
83
- PageValidator.validations[name] ||= {}
76
+ PageValidator.validations[self] ||= {}
84
77
  end
85
78
 
86
79
  private
87
80
 
88
81
  def validate_element(element_name, value = nil)
89
82
  validations[:element_presence] =
90
- ->(web_page) { web_page.public_send(*["has_#{element_name}_element?", value].compact) }
83
+ lambda do |web_page, sync|
84
+ if sync
85
+ web_page.instance.public_send(*["has_#{element_name}_element?", value].compact)
86
+ else
87
+ !web_page.instance.public_send(*["has_no_#{element_name}_element?", value].compact)
88
+ end
89
+ end
91
90
  end
92
91
 
93
92
  def validate_by_url(pattern)
94
93
  validations[:url] =
95
- -> (web_page) { pattern === web_page.instance.current_url }
94
+ ->(web_page, _sync) { pattern === web_page.instance.current_url }
96
95
  end
97
96
 
98
97
  def validate_by_title(pattern)
99
98
  validations[:title] =
100
- -> (web_page) { pattern === web_page.instance.title }
99
+ ->(web_page, sync) { sync ? web_page.instance.has_title?(pattern) : pattern === web_page.instance.title }
101
100
  end
102
101
 
103
102
  def validate_by_type(type, value, additional_value)
@@ -1,18 +1,14 @@
1
+ require 'howitzer/web/capybara_context_holder'
1
2
  module Howitzer
2
3
  module Web
3
4
  # This module combines section dsl methods
4
5
  module SectionDsl
6
+ include CapybaraContextHolder
7
+
5
8
  def self.included(base) #:nodoc:
6
9
  base.extend(ClassMethods)
7
10
  end
8
11
 
9
- # Returns capybara context. For example, capybara session, parent element, etc.
10
- # @abstract should be defined in parent context
11
-
12
- def capybara_context
13
- raise NotImplementedError, "Please define 'capybara_context' method for class holder"
14
- end
15
-
16
12
  # This module holds section dsl class methods
17
13
  module ClassMethods
18
14
  # This class is for private usage only
@@ -1,10 +1,10 @@
1
1
  log_dir: "spec/log"
2
2
  driver: selenium
3
3
  mailgun_domain: mailgun@test.domain
4
- mailgun_idle_timeout: 0.5
5
4
  mailgun_sleep_time: 0.05
6
5
  page_load_idle_timeout: 0.1
7
6
  capybara_wait_time: 5
8
7
  cloud_http_idle_timeout: 10
9
8
  hide_datetime_from_log: true
10
9
  maximized_window: false
10
+ mailgun_idle_timeout: 0.5
data/spec/spec_helper.rb CHANGED
@@ -69,11 +69,5 @@ Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
69
69
  RSpec.configure do |config|
70
70
  config.include Howitzer::GeneratorHelper
71
71
  config.disable_monkey_patching = true
72
- config.around(:each) do |ex|
73
- $stdout = StringIO.new
74
- $stderr = StringIO.new
75
- ex.run
76
- $stdout = STDOUT
77
- $stderr = STDERR
78
- end
72
+ config.around(:each, &:run)
79
73
  end
@@ -12,8 +12,8 @@ RSpec.shared_examples :capybara_context_holder do
12
12
  context 'when parent class has the method' do
13
13
  before do
14
14
  web_page_class.class_eval do
15
- def capybara_context
16
- true
15
+ def capybara_scopes
16
+ @_scopes ||= [true]
17
17
  end
18
18
  end
19
19
  end
@@ -25,7 +25,7 @@ RSpec.shared_examples :capybara_context_holder do
25
25
  it 'should raise error' do
26
26
  expect { subject }.to raise_error(
27
27
  NotImplementedError,
28
- "Please define 'capybara_context' method for class holder"
28
+ "Please define 'capybara_scopes' method for class holder"
29
29
  )
30
30
  end
31
31
  end
@@ -14,6 +14,9 @@ RSpec.shared_examples :element_dsl do
14
14
  it 'should create private :foo_elements instance method' do
15
15
  expect(klass_object.private_methods(false)).to include(:foo_elements)
16
16
  end
17
+ it 'should create private :wait_for_foo_element instance method' do
18
+ expect(klass_object.private_methods(false)).to include(:wait_for_foo_element)
19
+ end
17
20
  it 'should create public :has_foo_element? instance method' do
18
21
  expect(klass_object.public_methods(false)).to include(:has_foo_element?)
19
22
  end
@@ -37,6 +40,9 @@ RSpec.shared_examples :element_dsl do
37
40
  it 'should create private :foo_elements instance method' do
38
41
  expect(klass_object.private_methods(false)).to include(:foo_elements)
39
42
  end
43
+ it 'should create private :wait_for_foo_element instance method' do
44
+ expect(klass_object.private_methods(false)).to include(:wait_for_foo_element)
45
+ end
40
46
  it 'should create public :has_foo_element? instance method' do
41
47
  expect(klass_object.public_methods(false)).to include(:has_foo_element?)
42
48
  end
@@ -69,50 +75,154 @@ RSpec.shared_examples :element_dsl do
69
75
  before do
70
76
  allow(Capybara).to receive(:current_session) { kontext }
71
77
  klass.class_eval do
72
- element :foo, :xpath, ->(title) { "//a[.='#{title}']" }
78
+ element :foo, :xpath, ->(title, name) { "//a[.='#{title}']/*[@name='#{name}']" }
73
79
  element :bar, '.someclass'
80
+ element :top_panel, '.top'
74
81
  end
75
82
  end
76
- after { subject }
77
83
 
78
84
  describe '#name_element' do
79
85
  context 'when simple selector' do
80
- subject { klass_object.send(:bar_element) }
81
- it { expect(kontext).to receive(:find).with('.someclass') }
86
+ subject { klass_object.send(:bar_element, wait: 10) }
87
+ after { subject }
88
+ it { expect(klass_object.capybara_context).to receive(:find).with('.someclass', wait: 10) }
82
89
  end
83
90
  context 'when lambda selector' do
84
- subject { klass_object.send(:foo_element, 'Hello') }
85
- it { expect(kontext).to receive(:find).with(:xpath, "//a[.='Hello']") }
91
+ subject { klass_object.send(:foo_element, 'Hello', 'super', wait: 10) }
92
+ after { subject }
93
+ it do
94
+ expect(
95
+ klass_object.capybara_context
96
+ ).to receive(:find).with(:xpath, "//a[.='Hello']/*[@name='super']", wait: 10)
97
+ end
98
+
99
+ context 'when several execution with different data' do
100
+ it 'does not cache previous data' do
101
+ expect(
102
+ klass_object.capybara_context
103
+ ).to receive(:find).with(:xpath, "//a[.='Hello']/*[@name='super']", wait: 10).at_least(:once)
104
+ klass_object.send(:foo_element, 'Hello', 'super', wait: 10)
105
+
106
+ expect(
107
+ klass_object.capybara_context
108
+ ).to receive(:find).with(:xpath, "//a[.='Bye']/*[@name='puper']", wait: 15).at_least(:once)
109
+ klass_object.send(:foo_element, 'Bye', 'puper', wait: 15)
110
+ end
111
+ end
86
112
  end
87
113
  end
88
114
  describe '#name_elements' do
115
+ after { subject }
89
116
  context 'when simple selector' do
90
- subject { klass_object.send(:bar_elements) }
91
- it { expect(kontext).to receive(:all).with('.someclass') }
117
+ subject { klass_object.send(:bar_elements, wait: 10) }
118
+ it { expect(klass_object.capybara_context).to receive(:all).with('.someclass', wait: 10) }
92
119
  end
93
120
  context 'when lambda selector' do
94
- subject { klass_object.send(:foo_elements, 'Hello') }
95
- it { expect(kontext).to receive(:all).with(:xpath, "//a[.='Hello']") }
121
+ subject { klass_object.send(:foo_elements, 'Hello', 'super', wait: 10) }
122
+ it do
123
+ expect(
124
+ klass_object.capybara_context
125
+ ).to receive(:all).with(:xpath, "//a[.='Hello']/*[@name='super']", wait: 10)
126
+ end
127
+ end
128
+ end
129
+ describe '#wait_for_name_element' do
130
+ context 'when simple selector' do
131
+ subject { klass_object.send(:wait_for_bar_element, wait: 10) }
132
+ it do
133
+ expect(klass_object.capybara_context).to receive(:find).with('.someclass', wait: 10)
134
+ is_expected.to eq(nil)
135
+ end
136
+ end
137
+ context 'when lambda selector' do
138
+ subject { klass_object.send(:wait_for_foo_element, 'Hello', 'super', wait: 10) }
139
+ it do
140
+ expect(
141
+ klass_object.capybara_context
142
+ ).to receive(:find).with(:xpath, "//a[.='Hello']/*[@name='super']", wait: 10)
143
+ is_expected.to eq(nil)
144
+ end
145
+ end
146
+ end
147
+ describe '#within_name_element' do
148
+ let(:within_scope) { double }
149
+ after { subject }
150
+ context 'not nested' do
151
+ context 'when simple selector' do
152
+ subject do
153
+ klass_object.instance_eval do
154
+ within_bar_element(wait: 10) do
155
+ foo_element('Hello', 'super', wait: 10)
156
+ end
157
+ end
158
+ end
159
+ it do
160
+ expect(klass_object.capybara_context).to receive(:find).with('.someclass', wait: 10) { within_scope }
161
+ expect(within_scope).to receive(:find).with(:xpath, "//a[.='Hello']/*[@name='super']", wait: 10)
162
+ end
163
+ end
164
+ context 'when lambda selector' do
165
+ subject do
166
+ klass_object.instance_eval do
167
+ within_foo_element('Hello', 'super', wait: 10) do
168
+ bar_element(wait: 10)
169
+ end
170
+ end
171
+ end
172
+ it do
173
+ expect(
174
+ klass_object.capybara_context
175
+ ).to receive(:find).with(:xpath, "//a[.='Hello']/*[@name='super']", wait: 10) { within_scope }
176
+ expect(within_scope).to receive(:find).with('.someclass', wait: 10)
177
+ end
178
+ end
179
+ end
180
+ context 'nested' do
181
+ let(:nested_within_scope) { double }
182
+ subject do
183
+ klass_object.instance_eval do
184
+ within_top_panel_element(wait: 10) do
185
+ within_bar_element(wait: 10) do
186
+ foo_element('Hello', 'super', wait: 10)
187
+ end
188
+ end
189
+ end
190
+ end
191
+ it do
192
+ expect(klass_object.capybara_context).to receive(:find).with('.top', wait: 10) { within_scope }
193
+ expect(within_scope).to receive(:find).with('.someclass', wait: 10) { nested_within_scope }
194
+ expect(nested_within_scope).to receive(:find).with(:xpath, "//a[.='Hello']/*[@name='super']", wait: 10)
195
+ end
96
196
  end
97
197
  end
98
198
  describe '#has_name_element?' do
199
+ after { subject }
99
200
  context 'when simple selector' do
100
- subject { klass_object.send(:has_bar_element?) }
101
- it { expect(kontext).to receive(:has_selector?).with('.someclass') }
201
+ subject { klass_object.send(:has_bar_element?, wait: 10) }
202
+ it { expect(klass_object.capybara_context).to receive(:has_selector?).with('.someclass', wait: 10) }
102
203
  end
103
204
  context 'when lambda selector' do
104
- subject { klass_object.send(:has_foo_element?, 'Hello') }
105
- it { expect(kontext).to receive(:has_selector?).with(:xpath, "//a[.='Hello']") }
205
+ subject { klass_object.send(:has_foo_element?, 'Hello', 'super', wait: 10) }
206
+ it do
207
+ expect(
208
+ klass_object.capybara_context
209
+ ).to receive(:has_selector?).with(:xpath, "//a[.='Hello']/*[@name='super']", wait: 10)
210
+ end
106
211
  end
107
212
  end
108
213
  describe '#has_no_name_element?' do
214
+ after { subject }
109
215
  context 'when simple selector' do
110
- subject { klass_object.send(:has_no_bar_element?) }
111
- it { expect(kontext).to receive(:has_no_selector?).with('.someclass') }
216
+ subject { klass_object.send(:has_no_bar_element?, wait: 10) }
217
+ it { expect(klass_object.capybara_context).to receive(:has_no_selector?).with('.someclass', wait: 10) }
112
218
  end
113
219
  context 'when lambda selector' do
114
- subject { klass_object.send(:has_no_foo_element?, 'Hello') }
115
- it { expect(kontext).to receive(:has_no_selector?).with(:xpath, "//a[.='Hello']") }
220
+ subject { klass_object.send(:has_no_foo_element?, 'Hello', 'super', wait: 10) }
221
+ it do
222
+ expect(
223
+ klass_object.capybara_context
224
+ ).to receive(:has_no_selector?).with(:xpath, "//a[.='Hello']/*[@name='super']", wait: 10)
225
+ end
116
226
  end
117
227
  end
118
228
  end
@@ -52,19 +52,19 @@ RSpec.describe Howitzer::BaseGenerator do
52
52
 
53
53
  describe '#manifest' do
54
54
  subject { described_class.new({}).manifest }
55
- before { allow_any_instance_of(described_class).to receive(:initialize) { nil } }
56
- it { is_expected.to be_nil }
55
+ before { allow_any_instance_of(described_class).to receive(:print_banner) { nil } }
56
+ it { is_expected.to eq([]) }
57
57
  end
58
58
 
59
59
  describe '#banner' do
60
60
  subject { described_class.new({}).send(:banner) }
61
- before { allow_any_instance_of(described_class).to receive(:initialize) { nil } }
61
+ before { allow_any_instance_of(described_class).to receive(:print_banner) { nil } }
62
62
  it { is_expected.to be_nil }
63
63
  end
64
64
 
65
65
  describe '#logger' do
66
66
  subject { described_class.new({}).send(:logger) }
67
- before { allow_any_instance_of(described_class).to receive(:initialize) { nil } }
67
+ before { allow_any_instance_of(described_class).to receive(:print_banner) { nil } }
68
68
  context 'when not specified' do
69
69
  before { described_class.instance_variable_set('@logger', nil) }
70
70
  it { is_expected.to eq($stdout) }
@@ -79,7 +79,7 @@ RSpec.describe Howitzer::BaseGenerator do
79
79
  describe '#destination' do
80
80
  subject { described_class.new({}).send(:destination) }
81
81
  before do
82
- allow_any_instance_of(described_class).to receive(:initialize) { nil }
82
+ allow_any_instance_of(described_class).to receive(:print_banner) { nil }
83
83
  allow(described_class).to receive(:destination) { '/' }
84
84
  end
85
85
  it { is_expected.to eq('/') }
@@ -91,7 +91,7 @@ RSpec.describe Howitzer::BaseGenerator do
91
91
  let(:generator) { described_class.new({}) }
92
92
  subject { generator.send(:copy_files, list) }
93
93
  before do
94
- allow_any_instance_of(described_class).to receive(:initialize) { nil }
94
+ allow_any_instance_of(described_class).to receive(:print_banner) { nil }
95
95
  allow(generator).to receive(:source_path).with(list.first[:source]) { source_path }
96
96
  end
97
97
  after { subject }
@@ -112,7 +112,7 @@ RSpec.describe Howitzer::BaseGenerator do
112
112
  let(:generator) { described_class.new('rspec' => true) }
113
113
  subject { generator.send(:copy_templates, list) }
114
114
  before do
115
- allow_any_instance_of(described_class).to receive(:initialize) { nil }
115
+ allow_any_instance_of(described_class).to receive(:print_banner) { nil }
116
116
  allow(generator).to receive(:source_path).with(list.first[:source]) { source_path }
117
117
  allow(generator).to receive(:dest_path).with(list.first[:destination]) { destination_path }
118
118
  allow(generator).to receive(:write_template).with(destination_path, source_path)
@@ -147,13 +147,12 @@ RSpec.describe Howitzer::BaseGenerator do
147
147
  let(:generator) { described_class.new({}) }
148
148
  subject { generator.send(:print_banner) }
149
149
  before do
150
- allow_any_instance_of(described_class).to receive(:initialize) { nil }
151
- allow(generator).to receive(:banner) { banner }
150
+ allow_any_instance_of(described_class).to receive(:banner) { banner }
152
151
  end
153
152
  after { subject }
154
153
  context 'when banner present' do
155
154
  let(:banner) { 'banner' }
156
- it { expect(described_class.logger).to receive(:puts).with(banner).once }
155
+ it { expect(described_class.logger).to receive(:puts).with(banner).twice }
157
156
  end
158
157
  context 'when banner blank' do
159
158
  let(:banner) { '' }
@@ -163,21 +162,21 @@ RSpec.describe Howitzer::BaseGenerator do
163
162
 
164
163
  describe '#print_info' do
165
164
  subject { described_class.new({}).send(:print_info, 'data') }
166
- before { allow_any_instance_of(described_class).to receive(:initialize) { nil } }
165
+ before { allow_any_instance_of(described_class).to receive(:print_banner) { nil } }
167
166
  after { subject }
168
167
  it { expect(described_class.logger).to receive(:print).with(' data') }
169
168
  end
170
169
 
171
170
  describe '#puts_info' do
172
171
  subject { described_class.new({}).send(:puts_info, 'data') }
173
- before { allow_any_instance_of(described_class).to receive(:initialize) { nil } }
172
+ before { allow_any_instance_of(described_class).to receive(:print_banner) { nil } }
174
173
  after { subject }
175
174
  it { expect(described_class.logger).to receive(:puts).with(' data') }
176
175
  end
177
176
 
178
177
  describe '#puts_error' do
179
178
  subject { described_class.new({}).send(:puts_error, 'data') }
180
- before { allow_any_instance_of(described_class).to receive(:initialize) { nil } }
179
+ before { allow_any_instance_of(described_class).to receive(:print_banner) { nil } }
181
180
  after { subject }
182
181
  it { expect(described_class.logger).to receive(:puts).with(' ERROR: data') }
183
182
  end
@@ -185,14 +184,14 @@ RSpec.describe Howitzer::BaseGenerator do
185
184
  describe '#source_path' do
186
185
  subject { described_class.new({}).send(:source_path, 'example.txt') }
187
186
  before do
188
- allow_any_instance_of(described_class).to receive(:initialize) { nil }
187
+ allow_any_instance_of(described_class).to receive(:print_banner) { nil }
189
188
  end
190
189
  it { is_expected.to include('/base/templates/example.txt') }
191
190
  end
192
191
 
193
192
  describe '#dest_path' do
194
193
  subject { described_class.new({}).send(:dest_path, 'example.txt') }
195
- before { allow_any_instance_of(described_class).to receive(:initialize) { nil } }
194
+ before { allow_any_instance_of(described_class).to receive(:print_banner) { nil } }
196
195
  it { is_expected.to include('/example.txt') }
197
196
  end
198
197
 
@@ -203,7 +202,7 @@ RSpec.describe Howitzer::BaseGenerator do
203
202
  let(:dst) { '/path/to/d.txt' }
204
203
  subject { generator.send(:copy_with_path, data) }
205
204
  before do
206
- allow_any_instance_of(described_class).to receive(:initialize) { nil }
205
+ allow_any_instance_of(described_class).to receive(:print_banner) { nil }
207
206
  allow(generator).to receive(:source_path).with('s.txt') { src }
208
207
  allow(generator).to receive(:dest_path).with('d.txt') { dst }
209
208
  allow(FileUtils).to receive(:mkdir_p).with('/path/to') { true }