hanami 2.0.2 → 2.1.0.beta1

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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +25 -9
  5. data/hanami.gemspec +2 -2
  6. data/lib/hanami/config/actions.rb +0 -4
  7. data/lib/hanami/config/logger.rb +1 -1
  8. data/lib/hanami/config/views.rb +0 -4
  9. data/lib/hanami/config.rb +54 -0
  10. data/lib/hanami/extensions/action/slice_configured_action.rb +15 -7
  11. data/lib/hanami/extensions/action.rb +4 -4
  12. data/lib/hanami/extensions/router/errors.rb +58 -0
  13. data/lib/hanami/extensions/view/context.rb +129 -60
  14. data/lib/hanami/extensions/view/part.rb +26 -0
  15. data/lib/hanami/extensions/view/scope.rb +26 -0
  16. data/lib/hanami/extensions/view/slice_configured_context.rb +0 -2
  17. data/lib/hanami/extensions/view/slice_configured_helpers.rb +44 -0
  18. data/lib/hanami/extensions/view/slice_configured_view.rb +106 -21
  19. data/lib/hanami/extensions/view/standard_helpers.rb +14 -0
  20. data/lib/hanami/extensions.rb +10 -3
  21. data/lib/hanami/helpers/form_helper/form_builder.rb +1391 -0
  22. data/lib/hanami/helpers/form_helper/values.rb +75 -0
  23. data/lib/hanami/helpers/form_helper.rb +213 -0
  24. data/lib/hanami/middleware/public_errors_app.rb +75 -0
  25. data/lib/hanami/middleware/render_errors.rb +93 -0
  26. data/lib/hanami/slice.rb +28 -2
  27. data/lib/hanami/slice_configurable.rb +3 -2
  28. data/lib/hanami/version.rb +1 -1
  29. data/lib/hanami/web/rack_logger.rb +8 -20
  30. data/lib/hanami.rb +1 -1
  31. data/spec/integration/action/view_rendering/view_context_spec.rb +221 -0
  32. data/spec/integration/action/view_rendering_spec.rb +0 -18
  33. data/spec/integration/rack_app/middleware_spec.rb +23 -23
  34. data/spec/integration/rack_app/rack_app_spec.rb +5 -1
  35. data/spec/integration/slices/slice_registrations_spec.rb +80 -0
  36. data/spec/integration/view/config/default_context_spec.rb +149 -0
  37. data/spec/integration/view/{inflector_spec.rb → config/inflector_spec.rb} +1 -1
  38. data/spec/integration/view/config/part_class_spec.rb +147 -0
  39. data/spec/integration/view/config/part_namespace_spec.rb +103 -0
  40. data/spec/integration/view/config/paths_spec.rb +119 -0
  41. data/spec/integration/view/config/scope_class_spec.rb +147 -0
  42. data/spec/integration/view/config/scope_namespace_spec.rb +103 -0
  43. data/spec/integration/view/config/template_spec.rb +38 -0
  44. data/spec/integration/view/context/request_spec.rb +3 -7
  45. data/spec/integration/view/helpers/form_helper_spec.rb +174 -0
  46. data/spec/integration/view/helpers/part_helpers_spec.rb +124 -0
  47. data/spec/integration/view/helpers/scope_helpers_spec.rb +84 -0
  48. data/spec/integration/view/helpers/user_defined_helpers/part_helpers_spec.rb +162 -0
  49. data/spec/integration/view/helpers/user_defined_helpers/scope_helpers_spec.rb +119 -0
  50. data/spec/integration/view/slice_configuration_spec.rb +9 -9
  51. data/spec/integration/web/render_detailed_errors_spec.rb +90 -0
  52. data/spec/integration/web/render_errors_spec.rb +240 -0
  53. data/spec/spec_helper.rb +1 -1
  54. data/spec/support/matchers.rb +32 -0
  55. data/spec/unit/hanami/config/actions/default_values_spec.rb +0 -4
  56. data/spec/unit/hanami/config/logger_spec.rb +9 -0
  57. data/spec/unit/hanami/config/render_detailed_errors_spec.rb +25 -0
  58. data/spec/unit/hanami/config/render_errors_spec.rb +25 -0
  59. data/spec/unit/hanami/config/views_spec.rb +0 -18
  60. data/spec/unit/hanami/extensions/view/context_spec.rb +59 -0
  61. data/spec/unit/hanami/helpers/form_helper_spec.rb +2826 -0
  62. data/spec/unit/hanami/router/errors/not_allowed_error_spec.rb +27 -0
  63. data/spec/unit/hanami/router/errors/not_found_error_spec.rb +22 -0
  64. data/spec/unit/hanami/slice_configurable_spec.rb +18 -0
  65. data/spec/unit/hanami/version_spec.rb +1 -1
  66. data/spec/unit/hanami/web/rack_logger_spec.rb +1 -1
  67. metadata +67 -33
  68. data/spec/integration/action/view_integration_spec.rb +0 -165
  69. data/spec/integration/view/part_namespace_spec.rb +0 -96
  70. data/spec/integration/view/path_spec.rb +0 -56
  71. data/spec/integration/view/template_spec.rb +0 -68
  72. data/spec/isolation/hanami/application/already_configured_spec.rb +0 -19
  73. data/spec/isolation/hanami/application/inherit_anonymous_class_spec.rb +0 -10
  74. data/spec/isolation/hanami/application/inherit_concrete_class_spec.rb +0 -14
  75. data/spec/isolation/hanami/application/not_configured_spec.rb +0 -9
  76. data/spec/isolation/hanami/application/routes/configured_spec.rb +0 -44
  77. data/spec/isolation/hanami/application/routes/not_configured_spec.rb +0 -16
  78. data/spec/isolation/hanami/boot/success_spec.rb +0 -50
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rack/test"
4
+ require "stringio"
5
+
6
+ RSpec.describe "Helpers / FormHelper", :app_integration do
7
+ include Rack::Test::Methods
8
+ let(:app) { Hanami.app }
9
+
10
+ before do
11
+ with_directory(make_tmp_directory) do
12
+ write "config/app.rb", <<~RUBY
13
+ module TestApp
14
+ class App < Hanami::App
15
+ config.logger.stream = StringIO.new
16
+ end
17
+ end
18
+ RUBY
19
+
20
+ write "config/routes.rb", <<~RUBY
21
+ module TestApp
22
+ class Routes < Hanami::Routes
23
+ get "posts/:id/edit", to: "posts.edit"
24
+ put "posts/:id", to: "posts.update"
25
+ end
26
+ end
27
+ RUBY
28
+
29
+ write "app/action.rb", <<~RUBY
30
+ # auto_register: false
31
+
32
+ require "hanami/action"
33
+
34
+ module TestApp
35
+ class Action < Hanami::Action
36
+ end
37
+ end
38
+ RUBY
39
+
40
+ write "app/view.rb", <<~RUBY
41
+ # auto_register: false
42
+
43
+ require "hanami/view"
44
+
45
+ module TestApp
46
+ class View < Hanami::View
47
+ config.layout = nil
48
+ end
49
+ end
50
+ RUBY
51
+
52
+ write "app/actions/posts/edit.rb", <<~RUBY
53
+ module TestApp
54
+ module Actions
55
+ module Posts
56
+ class Edit < TestApp::Action
57
+ end
58
+ end
59
+ end
60
+ end
61
+ RUBY
62
+
63
+ write "app/actions/posts/update.rb", <<~RUBY
64
+ module TestApp
65
+ module Actions
66
+ module Posts
67
+ class Update < TestApp::Action
68
+ def handle(request, response)
69
+ if valid?(request.params[:post])
70
+ response.redirect_to "/posts/x/edit"
71
+ else
72
+ response.render view
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def valid?(post)
79
+ post.to_h[:title].to_s.length > 0
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ RUBY
86
+
87
+ write "app/views/posts/edit.rb", <<~RUBY
88
+ module TestApp
89
+ module Views
90
+ module Posts
91
+ class Edit < TestApp::View
92
+ expose :post do
93
+ Struct.new(:title, :body).new("Hello <world>", "This is the post.")
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ RUBY
100
+
101
+ write "app/templates/posts/edit.html.erb", <<~ERB
102
+ <h1>Edit post</h1>
103
+
104
+ <%= form_for("/posts") do |f| %>
105
+ <div>
106
+ Title:
107
+ <%= f.text_field "post.title" %>
108
+ </div>
109
+ <div>
110
+ Body:
111
+ <%= f.text_area "post.body" %>
112
+ </div>
113
+ <% end %>
114
+ ERB
115
+
116
+ before_prepare if respond_to?(:before_prepare)
117
+ require "hanami/prepare"
118
+ end
119
+ end
120
+
121
+ it "does not have a _csrf_token field when no sessions are configured" do
122
+ get "/posts/123/edit"
123
+
124
+ html = Capybara.string(last_response.body)
125
+
126
+ expect(html).to have_no_selector("input[type='hidden'][name='_csrf_token']")
127
+ end
128
+
129
+ it "uses the value from the view's locals" do
130
+ get "/posts/123/edit"
131
+
132
+ html = Capybara.string(last_response.body)
133
+
134
+ title_field = html.find("input[name='post[title]']")
135
+ expect(title_field.value).to eq "Hello <world>"
136
+
137
+ body_field = html.find("textarea[name='post[body]']")
138
+ expect(body_field.value).to eq "This is the post."
139
+ end
140
+
141
+ it "prefers the values from the request params" do
142
+ put "/posts/123", post: {title: "", body: "This is the UPDATED post."}
143
+
144
+ html = Capybara.string(last_response.body)
145
+
146
+ title_field = html.find("input[name='post[title]']")
147
+ expect(title_field.value).to eq ""
148
+
149
+ body_field = html.find("textarea[name='post[body]']")
150
+ expect(body_field.value).to eq "This is the UPDATED post."
151
+ end
152
+
153
+ context "sessions enabled" do
154
+ def before_prepare
155
+ write "config/app.rb", <<~RUBY
156
+ module TestApp
157
+ class App < Hanami::App
158
+ config.logger.stream = StringIO.new
159
+ config.actions.sessions = :cookie, {secret: "xyz"}
160
+ end
161
+ end
162
+ RUBY
163
+ end
164
+
165
+ it "inserts a CSRF token field" do
166
+ get "/posts/123/edit"
167
+
168
+ html = Capybara.string(last_response.body)
169
+
170
+ csrf_field = html.find("input[type='hidden'][name='_csrf_token']", visible: false)
171
+ expect(csrf_field.value).to match(/[a-z0-9]{10,}/i)
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ # rubocop:disable Style/OpenStructUse
6
+
7
+ RSpec.describe "App view / Helpers / Part helpers", :app_integration do
8
+ before do
9
+ with_directory(make_tmp_directory) do
10
+ write "config/app.rb", <<~RUBY
11
+ module TestApp
12
+ class App < Hanami::App
13
+ end
14
+ end
15
+ RUBY
16
+
17
+ write "app/view.rb", <<~RUBY
18
+ # auto_register: false
19
+
20
+ require "hanami/view"
21
+
22
+ module TestApp
23
+ class View < Hanami::View
24
+ config.layout = nil
25
+ end
26
+ end
27
+ RUBY
28
+
29
+ before_prepare if respond_to?(:before_prepare)
30
+ require "hanami/prepare"
31
+ end
32
+ end
33
+
34
+ describe "app view and parts" do
35
+ def before_prepare
36
+ write "app/views/posts/show.rb", <<~RUBY
37
+ module TestApp
38
+ module Views
39
+ module Posts
40
+ class Show < TestApp::View
41
+ expose :post
42
+ end
43
+ end
44
+ end
45
+ end
46
+ RUBY
47
+
48
+ write "app/views/parts/post.rb", <<~RUBY
49
+ module TestApp
50
+ module Views
51
+ module Parts
52
+ class Post < TestApp::Views::Part
53
+ def number
54
+ format_number(value.number)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ RUBY
61
+
62
+ write "app/templates/posts/show.html.erb", <<~ERB
63
+ <h1><%= post.number %></h1>
64
+ ERB
65
+ end
66
+
67
+ it "makes default helpers available in parts" do
68
+ post = OpenStruct.new(number: 12_345)
69
+ output = TestApp::App["views.posts.show"].call(post: post).to_s.strip
70
+
71
+ expect(output).to eq "<h1>12,345</h1>"
72
+ end
73
+ end
74
+
75
+ describe "slice view and parts" do
76
+ def before_prepare
77
+ write "slices/main/view.rb", <<~RUBY
78
+ module Main
79
+ class View < TestApp::View
80
+ end
81
+ end
82
+ RUBY
83
+
84
+ write "slices/main/views/posts/show.rb", <<~RUBY
85
+ module Main
86
+ module Views
87
+ module Posts
88
+ class Show < Main::View
89
+ expose :post
90
+ end
91
+ end
92
+ end
93
+ end
94
+ RUBY
95
+
96
+ write "slices/main/views/parts/post.rb", <<~RUBY
97
+ module Main
98
+ module Views
99
+ module Parts
100
+ class Post < Main::Views::Part
101
+ def number
102
+ format_number(value.number)
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ RUBY
109
+
110
+ write "slices/main/templates/posts/show.html.erb", <<~ERB
111
+ <h1><%= post.number %></h1>
112
+ ERB
113
+ end
114
+
115
+ it "makes default helpers available in parts" do
116
+ post = OpenStruct.new(number: 12_345)
117
+ output = Main::Slice["views.posts.show"].call(post: post).to_s.strip
118
+
119
+ expect(output).to eq "<h1>12,345</h1>"
120
+ end
121
+ end
122
+ end
123
+
124
+ # rubocop:enable Style/OpenStructUse
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe "App view / Helpers / Scope helpers", :app_integration do
4
+ before do
5
+ with_directory(make_tmp_directory) do
6
+ write "config/app.rb", <<~RUBY
7
+ module TestApp
8
+ class App < Hanami::App
9
+ end
10
+ end
11
+ RUBY
12
+
13
+ write "app/view.rb", <<~RUBY
14
+ # auto_register: false
15
+
16
+ require "hanami/view"
17
+
18
+ module TestApp
19
+ class View < Hanami::View
20
+ config.layout = nil
21
+ end
22
+ end
23
+ RUBY
24
+
25
+ before_prepare if respond_to?(:before_prepare)
26
+ require "hanami/prepare"
27
+ end
28
+ end
29
+
30
+ describe "app view" do
31
+ def before_prepare
32
+ write "app/views/posts/show.rb", <<~RUBY
33
+ module TestApp
34
+ module Views
35
+ module Posts
36
+ class Show < TestApp::View
37
+ end
38
+ end
39
+ end
40
+ end
41
+ RUBY
42
+
43
+ write "app/templates/posts/show.html.erb", <<~ERB
44
+ <h1><%= format_number(12_345) %></h1>
45
+ ERB
46
+ end
47
+
48
+ it "makes default helpers available in templates" do
49
+ output = TestApp::App["views.posts.show"].call.to_s.strip
50
+ expect(output).to eq "<h1>12,345</h1>"
51
+ end
52
+ end
53
+
54
+ describe "slice view" do
55
+ def before_prepare
56
+ write "slices/main/view.rb", <<~RUBY
57
+ module Main
58
+ class View < TestApp::View
59
+ end
60
+ end
61
+ RUBY
62
+
63
+ write "slices/main/views/posts/show.rb", <<~RUBY
64
+ module Main
65
+ module Views
66
+ module Posts
67
+ class Show < Main::View
68
+ end
69
+ end
70
+ end
71
+ end
72
+ RUBY
73
+
74
+ write "slices/main/templates/posts/show.html.erb", <<~ERB
75
+ <h1><%= format_number(12_345) %></h1>
76
+ ERB
77
+ end
78
+
79
+ it "makes default helpers available in templates" do
80
+ output = Main::Slice["views.posts.show"].call.to_s.strip
81
+ expect(output).to eq "<h1>12,345</h1>"
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ # rubocop:disable Style/OpenStructUse
6
+
7
+ RSpec.describe "App view / Helpers / User-defined helpers / Scope helpers", :app_integration do
8
+ before do
9
+ with_directory(make_tmp_directory) do
10
+ write "config/app.rb", <<~RUBY
11
+ module TestApp
12
+ class App < Hanami::App
13
+ end
14
+ end
15
+ RUBY
16
+
17
+ write "app/view.rb", <<~RUBY
18
+ # auto_register: false
19
+
20
+ require "hanami/view"
21
+
22
+ module TestApp
23
+ class View < Hanami::View
24
+ config.layout = nil
25
+ end
26
+ end
27
+ RUBY
28
+
29
+ write "app/views/helpers.rb", <<~'RUBY'
30
+ # auto_register: false
31
+
32
+ module TestApp
33
+ module Views
34
+ module Helpers
35
+ def exclaim_from_app(str)
36
+ tag.h1("#{str}! (app helper)")
37
+ end
38
+ end
39
+ end
40
+ end
41
+ RUBY
42
+
43
+ before_prepare if respond_to?(:before_prepare)
44
+ require "hanami/prepare"
45
+ end
46
+ end
47
+
48
+ describe "app view and parts" do
49
+ def before_prepare
50
+ write "app/views/posts/show.rb", <<~RUBY
51
+ module TestApp
52
+ module Views
53
+ module Posts
54
+ class Show < TestApp::View
55
+ expose :post
56
+ end
57
+ end
58
+ end
59
+ end
60
+ RUBY
61
+
62
+ write "app/views/parts/post.rb", <<~RUBY
63
+ module TestApp
64
+ module Views
65
+ module Parts
66
+ class Post < TestApp::Views::Part
67
+ def title
68
+ exclaim_from_app(value.title)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ RUBY
75
+
76
+ write "app/templates/posts/show.html.erb", <<~ERB
77
+ <%= post.title %>
78
+ ERB
79
+ end
80
+
81
+ it "makes user-defined helpers available in parts" do
82
+ post = OpenStruct.new(title: "Hello world")
83
+ output = TestApp::App["views.posts.show"].call(post: post).to_s.strip
84
+
85
+ expect(output).to eq "<h1>Hello world! (app helper)</h1>"
86
+ end
87
+ end
88
+
89
+ describe "slice view and parts" do
90
+ def before_prepare
91
+ write "slices/main/view.rb", <<~RUBY
92
+ module Main
93
+ class View < TestApp::View
94
+ # FIXME: base slice views should override paths from the base app view
95
+ config.paths = [File.join(File.expand_path(__dir__), "templates")]
96
+ end
97
+ end
98
+ RUBY
99
+
100
+ write "slices/main/views/helpers.rb", <<~'RUBY'
101
+ # auto_register: false
102
+
103
+ module Main
104
+ module Views
105
+ module Helpers
106
+ def exclaim_from_slice(str)
107
+ tag.h1("#{str}! (slice helper)")
108
+ end
109
+ end
110
+ end
111
+ end
112
+ RUBY
113
+
114
+ write "slices/main/views/posts/show.rb", <<~RUBY
115
+ module Main
116
+ module Views
117
+ module Posts
118
+ class Show < Main::View
119
+ expose :post
120
+ end
121
+ end
122
+ end
123
+ end
124
+ RUBY
125
+
126
+ write "slices/main/views/parts/post.rb", <<~RUBY
127
+ module Main
128
+ module Views
129
+ module Parts
130
+ class Post < Main::Views::Part
131
+ def title
132
+ exclaim_from_slice(value.title)
133
+ end
134
+
135
+ def title_from_app
136
+ exclaim_from_app(value.title)
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ RUBY
143
+
144
+ write "slices/main/templates/posts/show.html.erb", <<~ERB
145
+ <%= post.title %>
146
+ <%= post.title_from_app %>
147
+ ERB
148
+ end
149
+
150
+ it "makes user-defined helpers (from app as well as slice) available in parts" do
151
+ post = OpenStruct.new(title: "Hello world")
152
+ output = Main::Slice["views.posts.show"].call(post: post).to_s
153
+
154
+ expect(output).to eq <<~HTML
155
+ <h1>Hello world! (slice helper)</h1>
156
+ <h1>Hello world! (app helper)</h1>
157
+ HTML
158
+ end
159
+ end
160
+ end
161
+
162
+ # rubocop:enable Style/OpenStructUse
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe "App view / Helpers / User-defined helpers / Scope helpers", :app_integration do
4
+ before do
5
+ with_directory(make_tmp_directory) do
6
+ write "config/app.rb", <<~RUBY
7
+ module TestApp
8
+ class App < Hanami::App
9
+ end
10
+ end
11
+ RUBY
12
+
13
+ write "app/view.rb", <<~RUBY
14
+ # auto_register: false
15
+
16
+ require "hanami/view"
17
+
18
+ module TestApp
19
+ class View < Hanami::View
20
+ config.layout = nil
21
+ end
22
+ end
23
+ RUBY
24
+
25
+ write "app/views/helpers.rb", <<~'RUBY'
26
+ # auto_register: false
27
+
28
+ module TestApp
29
+ module Views
30
+ module Helpers
31
+ def exclaim_from_app(str)
32
+ tag.h1("#{str}! (app helper)")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ RUBY
38
+
39
+ before_prepare if respond_to?(:before_prepare)
40
+ require "hanami/prepare"
41
+ end
42
+ end
43
+
44
+ describe "app view" do
45
+ def before_prepare
46
+ write "app/views/posts/show.rb", <<~RUBY
47
+ module TestApp
48
+ module Views
49
+ module Posts
50
+ class Show < TestApp::View
51
+ end
52
+ end
53
+ end
54
+ end
55
+ RUBY
56
+
57
+ write "app/templates/posts/show.html.erb", <<~ERB
58
+ <%= exclaim_from_app("Hello world") %>
59
+ ERB
60
+ end
61
+
62
+ it "makes user-defined helpers available in templates" do
63
+ output = TestApp::App["views.posts.show"].call.to_s.strip
64
+ expect(output).to eq "<h1>Hello world! (app helper)</h1>"
65
+ end
66
+ end
67
+
68
+ describe "slice view" do
69
+ def before_prepare
70
+ write "slices/main/view.rb", <<~RUBY
71
+ module Main
72
+ class View < TestApp::View
73
+ # FIXME: base slice views should override paths from the base app view
74
+ config.paths = [File.join(File.expand_path(__dir__), "templates")]
75
+ end
76
+ end
77
+ RUBY
78
+
79
+ write "slices/main/views/helpers.rb", <<~'RUBY'
80
+ # auto_register: false
81
+
82
+ module Main
83
+ module Views
84
+ module Helpers
85
+ def exclaim_from_slice(str)
86
+ tag.h1("#{str}! (slice helper)")
87
+ end
88
+ end
89
+ end
90
+ end
91
+ RUBY
92
+
93
+ write "slices/main/views/posts/show.rb", <<~RUBY
94
+ module Main
95
+ module Views
96
+ module Posts
97
+ class Show < Main::View
98
+ end
99
+ end
100
+ end
101
+ end
102
+ RUBY
103
+
104
+ write "slices/main/templates/posts/show.html.erb", <<~ERB
105
+ <%= exclaim_from_slice("Hello world") %>
106
+ <%= exclaim_from_app("Hello world") %>
107
+ ERB
108
+ end
109
+
110
+ it "makes user-defined helpers (from app as well as slice) available in templates" do
111
+ output = Main::Slice["views.posts.show"].call.to_s
112
+
113
+ expect(output).to eq <<~HTML
114
+ <h1>Hello world! (slice helper)</h1>
115
+ <h1>Hello world! (app helper)</h1>
116
+ HTML
117
+ end
118
+ end
119
+ end