actionpack 4.2.10 → 7.2.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of actionpack might be problematic. Click here for more details.

Files changed (202) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +86 -600
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +13 -14
  5. data/lib/abstract_controller/asset_paths.rb +5 -1
  6. data/lib/abstract_controller/base.rb +166 -136
  7. data/lib/abstract_controller/caching/fragments.rb +149 -0
  8. data/lib/abstract_controller/caching.rb +68 -0
  9. data/lib/abstract_controller/callbacks.rb +126 -57
  10. data/lib/abstract_controller/collector.rb +13 -15
  11. data/lib/abstract_controller/deprecator.rb +9 -0
  12. data/lib/abstract_controller/error.rb +8 -0
  13. data/lib/abstract_controller/helpers.rb +181 -132
  14. data/lib/abstract_controller/logger.rb +5 -1
  15. data/lib/abstract_controller/railties/routes_helpers.rb +10 -3
  16. data/lib/abstract_controller/rendering.rb +56 -56
  17. data/lib/abstract_controller/translation.rb +29 -15
  18. data/lib/abstract_controller/url_for.rb +15 -11
  19. data/lib/abstract_controller.rb +21 -5
  20. data/lib/action_controller/api/api_rendering.rb +18 -0
  21. data/lib/action_controller/api.rb +154 -0
  22. data/lib/action_controller/base.rb +219 -155
  23. data/lib/action_controller/caching.rb +28 -68
  24. data/lib/action_controller/deprecator.rb +9 -0
  25. data/lib/action_controller/form_builder.rb +55 -0
  26. data/lib/action_controller/log_subscriber.rb +35 -22
  27. data/lib/action_controller/metal/allow_browser.rb +119 -0
  28. data/lib/action_controller/metal/basic_implicit_render.rb +17 -0
  29. data/lib/action_controller/metal/conditional_get.rb +259 -122
  30. data/lib/action_controller/metal/content_security_policy.rb +86 -0
  31. data/lib/action_controller/metal/cookies.rb +9 -5
  32. data/lib/action_controller/metal/data_streaming.rb +87 -104
  33. data/lib/action_controller/metal/default_headers.rb +21 -0
  34. data/lib/action_controller/metal/etag_with_flash.rb +22 -0
  35. data/lib/action_controller/metal/etag_with_template_digest.rb +35 -26
  36. data/lib/action_controller/metal/exceptions.rb +71 -24
  37. data/lib/action_controller/metal/flash.rb +26 -19
  38. data/lib/action_controller/metal/head.rb +45 -36
  39. data/lib/action_controller/metal/helpers.rb +80 -64
  40. data/lib/action_controller/metal/http_authentication.rb +297 -244
  41. data/lib/action_controller/metal/implicit_render.rb +57 -9
  42. data/lib/action_controller/metal/instrumentation.rb +76 -64
  43. data/lib/action_controller/metal/live.rb +238 -176
  44. data/lib/action_controller/metal/logging.rb +22 -0
  45. data/lib/action_controller/metal/mime_responds.rb +177 -166
  46. data/lib/action_controller/metal/parameter_encoding.rb +84 -0
  47. data/lib/action_controller/metal/params_wrapper.rb +145 -118
  48. data/lib/action_controller/metal/permissions_policy.rb +38 -0
  49. data/lib/action_controller/metal/rate_limiting.rb +62 -0
  50. data/lib/action_controller/metal/redirecting.rb +203 -64
  51. data/lib/action_controller/metal/renderers.rb +108 -65
  52. data/lib/action_controller/metal/rendering.rb +216 -56
  53. data/lib/action_controller/metal/request_forgery_protection.rb +496 -163
  54. data/lib/action_controller/metal/rescue.rb +19 -21
  55. data/lib/action_controller/metal/streaming.rb +179 -138
  56. data/lib/action_controller/metal/strong_parameters.rb +1058 -382
  57. data/lib/action_controller/metal/testing.rb +11 -17
  58. data/lib/action_controller/metal/url_for.rb +37 -21
  59. data/lib/action_controller/metal.rb +236 -138
  60. data/lib/action_controller/railtie.rb +89 -11
  61. data/lib/action_controller/railties/helpers.rb +5 -1
  62. data/lib/action_controller/renderer.rb +161 -0
  63. data/lib/action_controller/template_assertions.rb +13 -0
  64. data/lib/action_controller/test_case.rb +425 -497
  65. data/lib/action_controller.rb +44 -22
  66. data/lib/action_dispatch/constants.rb +34 -0
  67. data/lib/action_dispatch/deprecator.rb +9 -0
  68. data/lib/action_dispatch/http/cache.rb +119 -63
  69. data/lib/action_dispatch/http/content_disposition.rb +47 -0
  70. data/lib/action_dispatch/http/content_security_policy.rb +364 -0
  71. data/lib/action_dispatch/http/filter_parameters.rb +36 -34
  72. data/lib/action_dispatch/http/filter_redirect.rb +24 -12
  73. data/lib/action_dispatch/http/headers.rb +66 -31
  74. data/lib/action_dispatch/http/mime_negotiation.rb +106 -75
  75. data/lib/action_dispatch/http/mime_type.rb +196 -136
  76. data/lib/action_dispatch/http/mime_types.rb +25 -7
  77. data/lib/action_dispatch/http/parameters.rb +97 -45
  78. data/lib/action_dispatch/http/permissions_policy.rb +187 -0
  79. data/lib/action_dispatch/http/rack_cache.rb +6 -0
  80. data/lib/action_dispatch/http/request.rb +299 -170
  81. data/lib/action_dispatch/http/response.rb +311 -160
  82. data/lib/action_dispatch/http/upload.rb +52 -23
  83. data/lib/action_dispatch/http/url.rb +201 -125
  84. data/lib/action_dispatch/journey/formatter.rb +110 -50
  85. data/lib/action_dispatch/journey/gtg/builder.rb +37 -50
  86. data/lib/action_dispatch/journey/gtg/simulator.rb +20 -17
  87. data/lib/action_dispatch/journey/gtg/transition_table.rb +96 -36
  88. data/lib/action_dispatch/journey/nfa/dot.rb +5 -14
  89. data/lib/action_dispatch/journey/nodes/node.rb +100 -20
  90. data/lib/action_dispatch/journey/parser.rb +19 -17
  91. data/lib/action_dispatch/journey/parser.y +4 -3
  92. data/lib/action_dispatch/journey/parser_extras.rb +14 -4
  93. data/lib/action_dispatch/journey/path/pattern.rb +79 -63
  94. data/lib/action_dispatch/journey/route.rb +108 -44
  95. data/lib/action_dispatch/journey/router/utils.rb +41 -29
  96. data/lib/action_dispatch/journey/router.rb +64 -57
  97. data/lib/action_dispatch/journey/routes.rb +23 -21
  98. data/lib/action_dispatch/journey/scanner.rb +28 -17
  99. data/lib/action_dispatch/journey/visitors.rb +100 -54
  100. data/lib/action_dispatch/journey/visualizer/fsm.js +49 -24
  101. data/lib/action_dispatch/journey/visualizer/index.html.erb +1 -1
  102. data/lib/action_dispatch/journey.rb +7 -5
  103. data/lib/action_dispatch/log_subscriber.rb +25 -0
  104. data/lib/action_dispatch/middleware/actionable_exceptions.rb +46 -0
  105. data/lib/action_dispatch/middleware/assume_ssl.rb +27 -0
  106. data/lib/action_dispatch/middleware/callbacks.rb +7 -6
  107. data/lib/action_dispatch/middleware/cookies.rb +471 -328
  108. data/lib/action_dispatch/middleware/debug_exceptions.rb +149 -66
  109. data/lib/action_dispatch/middleware/debug_locks.rb +129 -0
  110. data/lib/action_dispatch/middleware/debug_view.rb +73 -0
  111. data/lib/action_dispatch/middleware/exception_wrapper.rb +275 -73
  112. data/lib/action_dispatch/middleware/executor.rb +32 -0
  113. data/lib/action_dispatch/middleware/flash.rb +143 -101
  114. data/lib/action_dispatch/middleware/host_authorization.rb +171 -0
  115. data/lib/action_dispatch/middleware/public_exceptions.rb +36 -27
  116. data/lib/action_dispatch/middleware/reloader.rb +10 -92
  117. data/lib/action_dispatch/middleware/remote_ip.rb +133 -107
  118. data/lib/action_dispatch/middleware/request_id.rb +29 -15
  119. data/lib/action_dispatch/middleware/server_timing.rb +78 -0
  120. data/lib/action_dispatch/middleware/session/abstract_store.rb +49 -27
  121. data/lib/action_dispatch/middleware/session/cache_store.rb +33 -16
  122. data/lib/action_dispatch/middleware/session/cookie_store.rb +86 -80
  123. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +15 -3
  124. data/lib/action_dispatch/middleware/show_exceptions.rb +66 -36
  125. data/lib/action_dispatch/middleware/ssl.rb +134 -36
  126. data/lib/action_dispatch/middleware/stack.rb +109 -44
  127. data/lib/action_dispatch/middleware/static.rb +159 -90
  128. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_message_and_suggestions.html.erb +22 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +7 -24
  132. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +1 -1
  133. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +36 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +46 -36
  136. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +12 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +9 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +26 -7
  139. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +3 -3
  140. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +16 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +139 -15
  143. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +23 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +6 -6
  146. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +7 -7
  147. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +9 -9
  148. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +1 -1
  149. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +4 -4
  150. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +1 -1
  151. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +7 -4
  152. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +125 -93
  153. data/lib/action_dispatch/railtie.rb +44 -16
  154. data/lib/action_dispatch/request/session.rb +159 -69
  155. data/lib/action_dispatch/request/utils.rb +97 -23
  156. data/lib/action_dispatch/routing/endpoint.rb +11 -2
  157. data/lib/action_dispatch/routing/inspector.rb +195 -106
  158. data/lib/action_dispatch/routing/mapper.rb +1338 -955
  159. data/lib/action_dispatch/routing/polymorphic_routes.rb +234 -201
  160. data/lib/action_dispatch/routing/redirection.rb +78 -51
  161. data/lib/action_dispatch/routing/route_set.rb +460 -374
  162. data/lib/action_dispatch/routing/routes_proxy.rb +36 -12
  163. data/lib/action_dispatch/routing/url_for.rb +172 -124
  164. data/lib/action_dispatch/routing.rb +159 -158
  165. data/lib/action_dispatch/system_test_case.rb +206 -0
  166. data/lib/action_dispatch/system_testing/browser.rb +84 -0
  167. data/lib/action_dispatch/system_testing/driver.rb +85 -0
  168. data/lib/action_dispatch/system_testing/server.rb +33 -0
  169. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +164 -0
  170. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +23 -0
  171. data/lib/action_dispatch/testing/assertion_response.rb +48 -0
  172. data/lib/action_dispatch/testing/assertions/response.rb +71 -39
  173. data/lib/action_dispatch/testing/assertions/routing.rb +228 -103
  174. data/lib/action_dispatch/testing/assertions.rb +9 -6
  175. data/lib/action_dispatch/testing/integration.rb +486 -306
  176. data/lib/action_dispatch/testing/request_encoder.rb +60 -0
  177. data/lib/action_dispatch/testing/test_helpers/page_dump_helper.rb +35 -0
  178. data/lib/action_dispatch/testing/test_process.rb +35 -22
  179. data/lib/action_dispatch/testing/test_request.rb +29 -34
  180. data/lib/action_dispatch/testing/test_response.rb +48 -15
  181. data/lib/action_dispatch.rb +82 -40
  182. data/lib/action_pack/gem_version.rb +8 -4
  183. data/lib/action_pack/version.rb +6 -2
  184. data/lib/action_pack.rb +21 -18
  185. metadata +146 -56
  186. data/lib/action_controller/caching/fragments.rb +0 -103
  187. data/lib/action_controller/metal/force_ssl.rb +0 -97
  188. data/lib/action_controller/metal/hide_actions.rb +0 -40
  189. data/lib/action_controller/metal/rack_delegation.rb +0 -32
  190. data/lib/action_controller/middleware.rb +0 -39
  191. data/lib/action_controller/model_naming.rb +0 -12
  192. data/lib/action_dispatch/http/parameter_filter.rb +0 -72
  193. data/lib/action_dispatch/journey/backwards.rb +0 -5
  194. data/lib/action_dispatch/journey/nfa/builder.rb +0 -76
  195. data/lib/action_dispatch/journey/nfa/simulator.rb +0 -47
  196. data/lib/action_dispatch/journey/nfa/transition_table.rb +0 -163
  197. data/lib/action_dispatch/journey/router/strexp.rb +0 -27
  198. data/lib/action_dispatch/middleware/params_parser.rb +0 -60
  199. data/lib/action_dispatch/middleware/templates/rescues/_source.erb +0 -27
  200. data/lib/action_dispatch/testing/assertions/dom.rb +0 -3
  201. data/lib/action_dispatch/testing/assertions/selector.rb +0 -3
  202. data/lib/action_dispatch/testing/assertions/tag.rb +0 -3
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionDispatch
6
+ module SystemTesting
7
+ class Browser # :nodoc:
8
+ attr_reader :name
9
+
10
+ def initialize(name)
11
+ @name = name
12
+ set_default_options
13
+ end
14
+
15
+ def type
16
+ case name
17
+ when :headless_chrome
18
+ :chrome
19
+ when :headless_firefox
20
+ :firefox
21
+ else
22
+ name
23
+ end
24
+ end
25
+
26
+ def options
27
+ @options ||=
28
+ case type
29
+ when :chrome
30
+ ::Selenium::WebDriver::Chrome::Options.new
31
+ when :firefox
32
+ ::Selenium::WebDriver::Firefox::Options.new
33
+ end
34
+ end
35
+
36
+ def configure
37
+ yield options if block_given?
38
+ end
39
+
40
+ # driver_path is lazily initialized by default. Eagerly set it to avoid race
41
+ # conditions when using parallel tests.
42
+ def preload
43
+ case type
44
+ when :chrome
45
+ resolve_driver_path(::Selenium::WebDriver::Chrome)
46
+ when :firefox
47
+ resolve_driver_path(::Selenium::WebDriver::Firefox)
48
+ end
49
+ end
50
+
51
+ private
52
+ def set_default_options
53
+ case name
54
+ when :headless_chrome
55
+ set_headless_chrome_browser_options
56
+ when :headless_firefox
57
+ set_headless_firefox_browser_options
58
+ end
59
+ end
60
+
61
+ def set_headless_chrome_browser_options
62
+ configure do |capabilities|
63
+ capabilities.add_argument("--headless")
64
+ capabilities.add_argument("--disable-gpu") if Gem.win_platform?
65
+ end
66
+ end
67
+
68
+ def set_headless_firefox_browser_options
69
+ configure do |capabilities|
70
+ capabilities.add_argument("-headless")
71
+ end
72
+ end
73
+
74
+ def resolve_driver_path(namespace)
75
+ # The path method has been deprecated in 4.20.0
76
+ if Gem::Version.new(::Selenium::WebDriver::VERSION) >= Gem::Version.new("4.20.0")
77
+ namespace::Service.driver_path = ::Selenium::WebDriver::DriverFinder.new(options, namespace::Service.new).driver_path
78
+ else
79
+ namespace::Service.driver_path = ::Selenium::WebDriver::DriverFinder.path(options, namespace::Service)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionDispatch
6
+ module SystemTesting
7
+ class Driver # :nodoc:
8
+ attr_reader :name
9
+
10
+ def initialize(driver_type, **options, &capabilities)
11
+ @driver_type = driver_type
12
+ @screen_size = options[:screen_size]
13
+ @options = options[:options] || {}
14
+ @name = @options.delete(:name) || driver_type
15
+ @capabilities = capabilities
16
+
17
+ if driver_type == :selenium
18
+ gem "selenium-webdriver", ">= 4.0.0"
19
+ require "selenium/webdriver"
20
+ @browser = Browser.new(options[:using])
21
+ @browser.preload unless @options[:browser] == :remote
22
+ else
23
+ @browser = nil
24
+ end
25
+ end
26
+
27
+ def use
28
+ register if registerable?
29
+
30
+ setup
31
+ end
32
+
33
+ private
34
+ def registerable?
35
+ [:selenium, :cuprite, :rack_test, :playwright].include?(@driver_type)
36
+ end
37
+
38
+ def register
39
+ @browser&.configure(&@capabilities)
40
+
41
+ Capybara.register_driver name do |app|
42
+ case @driver_type
43
+ when :selenium then register_selenium(app)
44
+ when :cuprite then register_cuprite(app)
45
+ when :rack_test then register_rack_test(app)
46
+ when :playwright then register_playwright(app)
47
+ end
48
+ end
49
+ end
50
+
51
+ def browser_options
52
+ @options.merge(options: @browser.options).compact
53
+ end
54
+
55
+ def register_selenium(app)
56
+ Capybara::Selenium::Driver.new(app, browser: @browser.type, **browser_options).tap do |driver|
57
+ driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size)
58
+ end
59
+ end
60
+
61
+ def register_cuprite(app)
62
+ Capybara::Cuprite::Driver.new(app, @options.merge(window_size: @screen_size))
63
+ end
64
+
65
+ def register_rack_test(app)
66
+ Capybara::RackTest::Driver.new(app, respect_data_method: true, **@options)
67
+ end
68
+
69
+ def register_playwright(app)
70
+ screen = { width: @screen_size[0], height: @screen_size[1] } if @screen_size
71
+ options = {
72
+ screen: screen,
73
+ viewport: screen,
74
+ **@options
75
+ }.compact
76
+
77
+ Capybara::Playwright::Driver.new(app, **options)
78
+ end
79
+
80
+ def setup
81
+ Capybara.current_driver = name
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionDispatch
6
+ module SystemTesting
7
+ class Server # :nodoc:
8
+ class << self
9
+ attr_accessor :silence_puma
10
+ end
11
+
12
+ self.silence_puma = false
13
+
14
+ def run
15
+ setup
16
+ end
17
+
18
+ private
19
+ def setup
20
+ set_server
21
+ set_port
22
+ end
23
+
24
+ def set_server
25
+ Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default]
26
+ end
27
+
28
+ def set_port
29
+ Capybara.always_include_port = true
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionDispatch
6
+ module SystemTesting
7
+ module TestHelpers
8
+ # Screenshot helper for system testing.
9
+ module ScreenshotHelper
10
+ # Takes a screenshot of the current page in the browser.
11
+ #
12
+ # `take_screenshot` can be used at any point in your system tests to take a
13
+ # screenshot of the current state. This can be useful for debugging or
14
+ # automating visual testing. You can take multiple screenshots per test to
15
+ # investigate changes at different points during your test. These will be named
16
+ # with a sequential prefix (or 'failed' for failing tests)
17
+ #
18
+ # The default screenshots directory is `tmp/screenshots` but you can set a
19
+ # different one with `Capybara.save_path`
20
+ #
21
+ # You can use the `html` argument or set the
22
+ # `RAILS_SYSTEM_TESTING_SCREENSHOT_HTML` environment variable to save the HTML
23
+ # from the page that is being screenshotted so you can investigate the elements
24
+ # on the page at the time of the screenshot
25
+ #
26
+ # You can use the `screenshot` argument or set the
27
+ # `RAILS_SYSTEM_TESTING_SCREENSHOT` environment variable to control the output.
28
+ # Possible values are:
29
+ # `simple` (default)
30
+ # : Only displays the screenshot path. This is the default value.
31
+ #
32
+ # `inline`
33
+ # : Display the screenshot in the terminal using the iTerm image protocol
34
+ # (https://iterm2.com/documentation-images.html).
35
+ #
36
+ # `artifact`
37
+ # : Display the screenshot in the terminal, using the terminal artifact
38
+ # format (https://buildkite.github.io/terminal-to-html/inline-images/).
39
+ #
40
+ #
41
+ def take_screenshot(html: false, screenshot: nil)
42
+ showing_html = html || html_from_env?
43
+
44
+ increment_unique
45
+ save_html if showing_html
46
+ save_image
47
+ show display_image(html: showing_html, screenshot_output: screenshot)
48
+ end
49
+
50
+ # Takes a screenshot of the current page in the browser if the test failed.
51
+ #
52
+ # `take_failed_screenshot` is called during system test teardown.
53
+ def take_failed_screenshot
54
+ return unless failed? && supports_screenshot? && Capybara::Session.instance_created?
55
+
56
+ take_screenshot
57
+ metadata[:failure_screenshot_path] = relative_image_path if Minitest::Runnable.method_defined?(:metadata)
58
+ end
59
+
60
+ private
61
+ attr_accessor :_screenshot_counter
62
+
63
+ def html_from_env?
64
+ ENV["RAILS_SYSTEM_TESTING_SCREENSHOT_HTML"] == "1"
65
+ end
66
+
67
+ def increment_unique
68
+ @_screenshot_counter ||= 0
69
+ @_screenshot_counter += 1
70
+ end
71
+
72
+ def unique
73
+ failed? ? "failures" : (_screenshot_counter || 0).to_s
74
+ end
75
+
76
+ def image_name
77
+ sanitized_method_name = method_name.gsub(/[^\w]+/, "-")
78
+ name = "#{unique}_#{sanitized_method_name}"
79
+ name[0...225]
80
+ end
81
+
82
+ def image_path
83
+ absolute_image_path.to_s
84
+ end
85
+
86
+ def html_path
87
+ absolute_html_path.to_s
88
+ end
89
+
90
+ def absolute_path
91
+ Rails.root.join(screenshots_dir, image_name)
92
+ end
93
+
94
+ def screenshots_dir
95
+ Capybara.save_path.presence || "tmp/screenshots"
96
+ end
97
+
98
+ def absolute_image_path
99
+ "#{absolute_path}.png"
100
+ end
101
+
102
+ def relative_image_path
103
+ "#{absolute_path.relative_path_from(Rails.root)}.png"
104
+ end
105
+
106
+ def absolute_html_path
107
+ "#{absolute_path}.html"
108
+ end
109
+
110
+ # rubocop:disable Lint/Debugger
111
+ def save_html
112
+ page.save_page(absolute_html_path)
113
+ end
114
+
115
+ def save_image
116
+ page.save_screenshot(absolute_image_path)
117
+ end
118
+ # rubocop:enable Lint/Debugger
119
+
120
+ def output_type
121
+ # Environment variables have priority
122
+ output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] || ENV["CAPYBARA_INLINE_SCREENSHOT"]
123
+
124
+ # Default to outputting a path to the screenshot
125
+ output_type ||= "simple"
126
+
127
+ output_type
128
+ end
129
+
130
+ def show(img)
131
+ puts img
132
+ end
133
+
134
+ def display_image(html:, screenshot_output:)
135
+ message = +"[Screenshot Image]: #{image_path}\n"
136
+ message << +"[Screenshot HTML]: #{html_path}\n" if html
137
+
138
+ case screenshot_output || output_type
139
+ when "artifact"
140
+ message << "\e]1338;url=artifact://#{absolute_image_path}\a\n"
141
+ when "inline"
142
+ name = inline_base64(File.basename(absolute_image_path))
143
+ image = inline_base64(File.read(absolute_image_path))
144
+ message << "\e]1337;File=name=#{name};height=400px;inline=1:#{image}\a\n"
145
+ end
146
+
147
+ message
148
+ end
149
+
150
+ def inline_base64(path)
151
+ Base64.strict_encode64(path)
152
+ end
153
+
154
+ def failed?
155
+ !passed? && !skipped?
156
+ end
157
+
158
+ def supports_screenshot?
159
+ Capybara.current_driver != :rack_test
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionDispatch
6
+ module SystemTesting
7
+ module TestHelpers
8
+ module SetupAndTeardown # :nodoc:
9
+ def before_teardown
10
+ take_failed_screenshot
11
+ ensure
12
+ super
13
+ end
14
+
15
+ def after_teardown
16
+ Capybara.reset_sessions!
17
+ ensure
18
+ super
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActionDispatch
6
+ # This is a class that abstracts away an asserted response. It purposely does
7
+ # not inherit from Response because it doesn't need it. That means it does not
8
+ # have headers or a body.
9
+ class AssertionResponse
10
+ attr_reader :code, :name
11
+
12
+ GENERIC_RESPONSE_CODES = { # :nodoc:
13
+ success: "2XX",
14
+ missing: "404",
15
+ redirect: "3XX",
16
+ error: "5XX"
17
+ }
18
+
19
+ # Accepts a specific response status code as an Integer (404) or String ('404')
20
+ # or a response status range as a Symbol pseudo-code (:success, indicating any
21
+ # 200-299 status code).
22
+ def initialize(code_or_name)
23
+ if code_or_name.is_a?(Symbol)
24
+ @name = code_or_name
25
+ @code = code_from_name(code_or_name)
26
+ else
27
+ @name = name_from_code(code_or_name)
28
+ @code = code_or_name
29
+ end
30
+
31
+ raise ArgumentError, "Invalid response name: #{name}" if @code.nil?
32
+ raise ArgumentError, "Invalid response code: #{code}" if @name.nil?
33
+ end
34
+
35
+ def code_and_name
36
+ "#{code}: #{name}"
37
+ end
38
+
39
+ private
40
+ def code_from_name(name)
41
+ GENERIC_RESPONSE_CODES[name] || Rack::Utils.status_code(name)
42
+ end
43
+
44
+ def name_from_code(code)
45
+ GENERIC_RESPONSE_CODES.invert[code] || Rack::Utils::HTTP_STATUS_CODES[code]
46
+ end
47
+ end
48
+ end
@@ -1,63 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
1
4
 
2
5
  module ActionDispatch
3
6
  module Assertions
4
- # A small suite of assertions that test responses from \Rails applications.
7
+ # A small suite of assertions that test responses from Rails applications.
5
8
  module ResponseAssertions
9
+ RESPONSE_PREDICATES = { # :nodoc:
10
+ success: :successful?,
11
+ missing: :not_found?,
12
+ redirect: :redirection?,
13
+ error: :server_error?,
14
+ }
15
+
6
16
  # Asserts that the response is one of the following types:
7
17
  #
8
- # * <tt>:success</tt> - Status code was in the 200-299 range
9
- # * <tt>:redirect</tt> - Status code was in the 300-399 range
10
- # * <tt>:missing</tt> - Status code was 404
11
- # * <tt>:error</tt> - Status code was in the 500-599 range
18
+ # * `:success` - Status code was in the 200-299 range
19
+ # * `:redirect` - Status code was in the 300-399 range
20
+ # * `:missing` - Status code was 404
21
+ # * `:error` - Status code was in the 500-599 range
22
+ #
12
23
  #
13
- # You can also pass an explicit status number like <tt>assert_response(501)</tt>
14
- # or its symbolic equivalent <tt>assert_response(:not_implemented)</tt>.
15
- # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list.
24
+ # You can also pass an explicit status number like `assert_response(501)` or its
25
+ # symbolic equivalent `assert_response(:not_implemented)`. See
26
+ # `Rack::Utils::SYMBOL_TO_STATUS_CODE` for a full list.
16
27
  #
17
- # # assert that the response was a redirection
18
- # assert_response :redirect
28
+ # # Asserts that the response was a redirection
29
+ # assert_response :redirect
19
30
  #
20
- # # assert that the response code was status code 401 (unauthorized)
21
- # assert_response 401
31
+ # # Asserts that the response code was status code 401 (unauthorized)
32
+ # assert_response 401
22
33
  def assert_response(type, message = nil)
23
- message ||= "Expected response to be a <#{type}>, but was <#{@response.response_code}>"
34
+ message ||= generate_response_message(type)
24
35
 
25
- if Symbol === type
26
- if [:success, :missing, :redirect, :error].include?(type)
27
- assert @response.send("#{type}?"), message
28
- else
29
- code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type]
30
- if code.nil?
31
- raise ArgumentError, "Invalid response type :#{type}"
32
- end
33
- assert_equal code, @response.response_code, message
34
- end
36
+ if RESPONSE_PREDICATES.key?(type)
37
+ assert @response.public_send(RESPONSE_PREDICATES[type]), message
35
38
  else
36
- assert_equal type, @response.response_code, message
39
+ assert_equal AssertionResponse.new(type).code, @response.response_code, message
37
40
  end
38
41
  end
39
42
 
40
- # Assert that the redirection options passed in match those of the redirect called in the latest action.
41
- # This match can be partial, such that <tt>assert_redirected_to(controller: "weblog")</tt> will also
42
- # match the redirection of <tt>redirect_to(controller: "weblog", action: "show")</tt> and so on.
43
+ # Asserts that the response is a redirect to a URL matching the given options.
43
44
  #
44
- # # assert that the redirection was to the "index" action on the WeblogController
45
- # assert_redirected_to controller: "weblog", action: "index"
45
+ # # Asserts that the redirection was to the "index" action on the WeblogController
46
+ # assert_redirected_to controller: "weblog", action: "index"
46
47
  #
47
- # # assert that the redirection was to the named route login_url
48
- # assert_redirected_to login_url
48
+ # # Asserts that the redirection was to the named route login_url
49
+ # assert_redirected_to login_url
49
50
  #
50
- # # assert that the redirection was to the url for @customer
51
- # assert_redirected_to @customer
51
+ # # Asserts that the redirection was to the URL for @customer
52
+ # assert_redirected_to @customer
52
53
  #
53
- # # asserts that the redirection matches the regular expression
54
- # assert_redirected_to %r(\Ahttp://example.org)
55
- def assert_redirected_to(options = {}, message=nil)
56
- assert_response(:redirect, message)
57
- return true if options === @response.location
54
+ # # Asserts that the redirection matches the regular expression
55
+ # assert_redirected_to %r(\Ahttp://example.org)
56
+ #
57
+ # # Asserts that the redirection has the HTTP status code 301 (Moved
58
+ # # Permanently).
59
+ # assert_redirected_to "/some/path", status: :moved_permanently
60
+ def assert_redirected_to(url_options = {}, options = {}, message = nil)
61
+ options, message = {}, options unless options.is_a?(Hash)
62
+
63
+ status = options[:status] || :redirect
64
+ assert_response(status, message)
65
+ return true if url_options === @response.location
58
66
 
59
67
  redirect_is = normalize_argument_to_redirection(@response.location)
60
- redirect_expected = normalize_argument_to_redirection(options)
68
+ redirect_expected = normalize_argument_to_redirection(url_options)
61
69
 
62
70
  message ||= "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>"
63
71
  assert_operator redirect_expected, :===, redirect_is, message
@@ -77,6 +85,30 @@ module ActionDispatch
77
85
  handle._compute_redirect_to_location(@request, fragment)
78
86
  end
79
87
  end
88
+
89
+ def generate_response_message(expected, actual = @response.response_code)
90
+ (+"Expected response to be a <#{code_with_name(expected)}>,"\
91
+ " but was a <#{code_with_name(actual)}>").concat(location_if_redirected).concat(response_body_if_short)
92
+ end
93
+
94
+ def response_body_if_short
95
+ return "" if @response.body.size > 500
96
+ "\nResponse body: #{@response.body}"
97
+ end
98
+
99
+ def location_if_redirected
100
+ return "" unless @response.redirection? && @response.location.present?
101
+ location = normalize_argument_to_redirection(@response.location)
102
+ " redirect to <#{location}>"
103
+ end
104
+
105
+ def code_with_name(code_or_name)
106
+ if RESPONSE_PREDICATES.value?("#{code_or_name}?".to_sym)
107
+ code_or_name = RESPONSE_PREDICATES.invert["#{code_or_name}?".to_sym]
108
+ end
109
+
110
+ AssertionResponse.new(code_or_name).code_and_name
111
+ end
80
112
  end
81
113
  end
82
114
  end