[904] Enable testing component JS interactions using new with_rendered_component_path method (#1061)
* Add render_in_browser test helper method to test JS interactions * Update test to test simple JS interactions * Fix rubocop violations * Add test coverage for with or without layout * Allow tmp file to be included to handle temp file generations * Fix more rubocop complaints * Exclude JS interaction test with layout specified * Change process_timeout on cuprite to address brittleness * Add new BrowserTestCase specified for in-browser tests of componnets * Attempt to utilize system test approach * Add selenium to appease failing specs (not sure why tbh!) * Bump to redo CI * Enable changing layout with a parameter in visit_rendered_in_browser * Update docs/CHANGELOG.md * Update docs/CHANGELOG.md * Update docs/CHANGELOG.md * Fix broken specs * Remove unneccesary .gitkeep on tmp * Create tmp folder if it doesn't exist * Fix linting error * Increase wait time on capybara & remove unneccesary comments * Add test coverage for when you use or not use a layout in system tests * Add guide on how to test interactions * Fix lint errors * Secure the system test endpoint and use specific directory for tmp components * Address rubocop violations * Fix problem with the mkdir * Update condition to check for being in production * Strictly only set routes used for system_tests under RAILS_ENV=test * Place the gems used for system_test to only be included in test environment * Update test to use new block syntax * Address linting issues reported by standardrb * Update Gemfile Co-authored-by: Joel Hawksley <joelhawksley@github.com> * Update Gemfile Co-authored-by: Joel Hawksley <joelhawksley@github.com> * Update app/controllers/view_components_controller.rb Co-authored-by: Joel Hawksley <joelhawksley@github.com> * Revert removal of tmp from .gitignore * Update Gemfile.lock * Revert gitignore change that is not needed * Update testing documentation * Apply renaming suggestions for entry point * Remove extra comment that is not needed * Further expand max_wait_time for capybara test to avoid test failures * Remove need to create dedicated folder in tmp for system test * Remove extra option for system test route drawing * Remove definition of template in with_rendered_component_in_browser * Update Gemfile.lock * Test * Add back in tmp directory to address test failures * Update Gemfile Co-authored-by: Joel Hawksley <joelhawksley@github.com> * Update test Gems to have coarse dependency pointers * Remove redundant status: 200 on system_test_entrypoint * Change test helper method to be clear why are not providing the browser, only the path! * Move the system_test_entrypoint to the PreviewsAction module * Conditionally use selenium 3 when using ruby 2.5 or below * Add option to incrase process_timeout on cuprite * Fix formatting issue on CHANGELOG.md * Update timeouts on cuprite * Update docs/CHANGELOG.md Co-authored-by: Joel Hawksley <joelhawksley@github.com> Update docs/guide/testing.md Co-authored-by: Joel Hawksley <joelhawksley@github.com> Update docs/guide/testing.md Co-authored-by: Joel Hawksley <joelhawksley@github.com> Update docs/guide/testing.md Co-authored-by: Joel Hawksley <joelhawksley@github.com> Update docs/guide/testing.md Co-authored-by: Joel Hawksley <joelhawksley@github.com> Update docs/guide/testing.md Co-authored-by: Joel Hawksley <joelhawksley@github.com> Update docs/guide/testing.md Co-authored-by: Joel Hawksley <joelhawksley@github.com> Update docs/guide/testing.md Co-authored-by: Joel Hawksley <joelhawksley@github.com> Update lib/view_component/engine.rb Co-authored-by: Joel Hawksley <joelhawksley@github.com> * Undo a change that was not intended * Update docs/guide/testing.md Co-authored-by: Joel Hawksley <joelhawksley@github.com> * Update docs/guide/testing.md Co-authored-by: Joel Hawksley <joelhawksley@github.com> * Decouple capybara from ViewComponents * Apply suggestions from code review * Add basic test for passing in parameters into component * Update API to use fragements to support slots * Update the method to be a bit more clear * Update for linting issues * Fix the method name * Fix linting issue * Place the html_safe in the class * Update API to match aligned naming * Update documentation for testing using new approach * Update yardoc * Remove empty line * Apply suggestions from code review * re-add errant removal * remove errant whitespace * render fragment directly instead of via preview template * reuse component instead of duplicating html * remove change to preview template * simplify example * simplify test helper * simplify test helper * prefix internal route with underscore * increase process_timeout Co-authored-by: Joel Hawksley <joel@hawksley.org> Co-authored-by: Joel Hawksley <joelhawksley@github.com>
This commit is contained in:
Родитель
8026219f3b
Коммит
93a67c39b9
12
Gemfile
12
Gemfile
|
@ -10,6 +10,18 @@ gem "rails", (rails_version == "main") ? {git: "https://github.com/rails/rails",
|
|||
|
||||
gem "rspec-rails", "~> 5"
|
||||
|
||||
group :test do
|
||||
gem "cuprite", "~> 0.8"
|
||||
gem "puma", "~> 5"
|
||||
|
||||
if RUBY_VERSION >= "2.6"
|
||||
gem "selenium-webdriver", "~> 4"
|
||||
else
|
||||
# Selenium 4 requires Ruby 2.6+
|
||||
gem "selenium-webdriver", "~> 3"
|
||||
end
|
||||
end
|
||||
|
||||
if RUBY_VERSION >= "3.1"
|
||||
gem "net-imap", require: false
|
||||
gem "net-pop", require: false
|
||||
|
|
39
Gemfile.lock
39
Gemfile.lock
|
@ -92,7 +92,7 @@ GEM
|
|||
parser (>= 2.4)
|
||||
smart_properties
|
||||
builder (3.2.4)
|
||||
capybara (3.37.1)
|
||||
capybara (3.38.0)
|
||||
addressable
|
||||
matrix
|
||||
mini_mime (>= 0.1.3)
|
||||
|
@ -101,9 +101,13 @@ GEM
|
|||
rack-test (>= 0.6.3)
|
||||
regexp_parser (>= 1.5, < 3.0)
|
||||
xpath (~> 3.2)
|
||||
childprocess (4.1.0)
|
||||
coderay (1.1.3)
|
||||
concurrent-ruby (1.1.10)
|
||||
crass (1.0.6)
|
||||
cuprite (0.14.3)
|
||||
capybara (~> 3.0)
|
||||
ferrum (~> 0.13.0)
|
||||
diff-lcs (1.5.0)
|
||||
docile (1.4.0)
|
||||
erb_lint (0.0.37)
|
||||
|
@ -115,6 +119,11 @@ GEM
|
|||
rubocop
|
||||
smart_properties
|
||||
erubi (1.11.0)
|
||||
ferrum (0.13)
|
||||
addressable (~> 2.5)
|
||||
concurrent-ruby (~> 1.1)
|
||||
webrick (~> 1.7)
|
||||
websocket-driver (>= 0.6, < 0.8)
|
||||
globalid (1.0.0)
|
||||
activesupport (>= 5.0)
|
||||
haml (5.2.2)
|
||||
|
@ -130,7 +139,7 @@ GEM
|
|||
loofah (2.19.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
m (1.6.0)
|
||||
m (1.6.1)
|
||||
method_source (>= 0.6.7)
|
||||
rake (>= 0.9.2.2)
|
||||
mail (2.7.1)
|
||||
|
@ -160,6 +169,8 @@ GEM
|
|||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
public_suffix (5.0.0)
|
||||
puma (5.6.5)
|
||||
nio4r (~> 2.0)
|
||||
racc (1.6.0)
|
||||
rack (2.2.4)
|
||||
rack-test (2.0.2)
|
||||
|
@ -192,7 +203,7 @@ GEM
|
|||
zeitwerk (~> 2.5)
|
||||
rainbow (3.1.1)
|
||||
rake (13.0.6)
|
||||
regexp_parser (2.6.0)
|
||||
regexp_parser (2.6.1)
|
||||
rexml (3.2.5)
|
||||
rspec-core (3.12.0)
|
||||
rspec-support (~> 3.12.0)
|
||||
|
@ -211,7 +222,7 @@ GEM
|
|||
rspec-mocks (~> 3.10)
|
||||
rspec-support (~> 3.10)
|
||||
rspec-support (3.12.0)
|
||||
rubocop (1.38.0)
|
||||
rubocop (1.39.0)
|
||||
json (~> 2.3)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 3.1.2.1)
|
||||
|
@ -223,10 +234,16 @@ GEM
|
|||
unicode-display_width (>= 1.4.0, < 3.0)
|
||||
rubocop-ast (1.23.0)
|
||||
parser (>= 3.1.1.0)
|
||||
rubocop-performance (1.15.0)
|
||||
rubocop-performance (1.15.1)
|
||||
rubocop (>= 1.7.0, < 2.0)
|
||||
rubocop-ast (>= 0.4.0)
|
||||
ruby-progressbar (1.11.0)
|
||||
rubyzip (2.3.2)
|
||||
selenium-webdriver (4.6.1)
|
||||
childprocess (>= 0.5, < 5.0)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
websocket (~> 1.0)
|
||||
simplecov (0.18.5)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
|
@ -246,9 +263,9 @@ GEM
|
|||
actionpack (>= 4.0)
|
||||
activesupport (>= 4.0)
|
||||
sprockets (>= 3.0.0)
|
||||
standard (1.17.0)
|
||||
rubocop (= 1.38.0)
|
||||
rubocop-performance (= 1.15.0)
|
||||
standard (1.18.1)
|
||||
rubocop (= 1.39.0)
|
||||
rubocop-performance (= 1.15.1)
|
||||
temple (0.8.2)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
|
@ -259,6 +276,7 @@ GEM
|
|||
concurrent-ruby (~> 1.0)
|
||||
unicode-display_width (2.3.0)
|
||||
webrick (1.7.0)
|
||||
websocket (1.2.9)
|
||||
websocket-driver (0.7.5)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
|
@ -268,7 +286,7 @@ GEM
|
|||
webrick (~> 1.7.0)
|
||||
yard-activesupport-concern (0.0.1)
|
||||
yard (>= 0.8)
|
||||
zeitwerk (2.6.4)
|
||||
zeitwerk (2.6.6)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
@ -279,15 +297,18 @@ DEPENDENCIES
|
|||
better_html (~> 1)
|
||||
bundler (~> 2)
|
||||
capybara (~> 3)
|
||||
cuprite (~> 0.8)
|
||||
erb_lint (~> 0.0.37)
|
||||
haml (~> 5)
|
||||
jbuilder (~> 2)
|
||||
m (~> 1)
|
||||
minitest (= 5.6.0)
|
||||
pry (~> 0.13)
|
||||
puma (~> 5)
|
||||
rails (~> 7.0.0)
|
||||
rake (~> 13.0)
|
||||
rspec-rails (~> 5)
|
||||
selenium-webdriver (~> 4)
|
||||
simplecov (~> 0.18.0)
|
||||
simplecov-console (~> 0.7.2)
|
||||
slim (~> 4.0)
|
||||
|
|
|
@ -39,6 +39,12 @@ module ViewComponent
|
|||
end
|
||||
end
|
||||
|
||||
if Rails.env.test?
|
||||
def system_test_entrypoint
|
||||
render file: "./tmp/view_components/#{params.permit(:file)[:file]}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# :doc:
|
||||
|
|
|
@ -10,6 +10,10 @@ nav_order: 5
|
|||
|
||||
## main
|
||||
|
||||
* Add `with_rendered_component_path` helper for writing component system tests.
|
||||
|
||||
*Edwin Mak*
|
||||
|
||||
* Include gem name and deprecation horizon in every deprecation message.
|
||||
|
||||
*Jan Klimo*
|
||||
|
|
|
@ -233,3 +233,33 @@ To use component previews:
|
|||
# config/application.rb
|
||||
config.view_component.preview_paths << "#{Rails.root}/spec/components/previews"
|
||||
```
|
||||
|
||||
## Component system tests
|
||||
|
||||
Use `with_rendered_component_path` with `render_inline` to system test components:
|
||||
|
||||
```rb
|
||||
class ViewComponentSystemTest < ViewComponent::SystemTestCase
|
||||
def test_simple_js_interaction_in_browser_without_layout
|
||||
with_rendered_component_path(render_inline(SimpleJavascriptInteractionWithJsIncludedComponent.new)) do |path|
|
||||
visit(path)
|
||||
|
||||
assert(find("[data-hidden-field]", visible: false))
|
||||
find("[data-button]", text: "Click Me To Reveal Something Cool").click
|
||||
assert(find("[data-hidden-field]", visible: true))
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
For components that depend on a layout, provide the `layout` argument:
|
||||
|
||||
```rb
|
||||
class ViewComponentSystemTest < ViewComponent::SystemTestCase
|
||||
def test_simple_js_interaction_in_browser_with_layout
|
||||
with_rendered_component_path(render_inline(SimpleJavascriptInteractionWithoutJsIncludedComponent.new), layout: 'application') do |path|
|
||||
# ...
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
|
|
@ -16,7 +16,9 @@ module ViewComponent
|
|||
autoload :Preview
|
||||
autoload :PreviewTemplateError
|
||||
autoload :TestHelpers
|
||||
autoload :SystemTestHelpers
|
||||
autoload :TestCase
|
||||
autoload :SystemTestCase
|
||||
autoload :TemplateError
|
||||
autoload :Translatable
|
||||
end
|
||||
|
|
|
@ -121,6 +121,10 @@ module ViewComponent
|
|||
internal: true
|
||||
)
|
||||
|
||||
if Rails.env.test?
|
||||
get("_system_test_entrypoint", to: "#{preview_controller}#system_test_entrypoint")
|
||||
end
|
||||
|
||||
get(
|
||||
"#{options.preview_route}/*path",
|
||||
to: "#{preview_controller}#previews",
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "active_support/test_case"
|
||||
|
||||
module ViewComponent
|
||||
class SystemTestCase < ActionDispatch::SystemTestCase
|
||||
include ViewComponent::SystemTestHelpers
|
||||
|
||||
def page
|
||||
Capybara.current_session
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ViewComponent
|
||||
module SystemTestHelpers
|
||||
include TestHelpers
|
||||
|
||||
#
|
||||
# Returns a block that can be used to visit the path of the inline rendered component.
|
||||
# @param fragment [Nokogiri::Fragment] The fragment returned from `render_inline`.
|
||||
# @param layout [String] The (optional) layout to use.
|
||||
# @return [Proc] A block that can be used to visit the path of the inline rendered component.
|
||||
def with_rendered_component_path(fragment, layout: false, &block)
|
||||
# Add './tmp/view_components/' directory if it doesn't exist to store the rendered component HTML
|
||||
FileUtils.mkdir_p("./tmp/view_components/") unless Dir.exist?("./tmp/view_components/")
|
||||
|
||||
file = Tempfile.new(["rendered_#{fragment.class.name}", ".html"], "tmp/view_components/")
|
||||
begin
|
||||
file.write(controller.render_to_string(html: fragment.to_html.html_safe, layout: layout))
|
||||
file.rewind
|
||||
|
||||
block.call("/_system_test_entrypoint?file=#{file.path.split("/").last}")
|
||||
ensure
|
||||
file.unlink
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function(event) {
|
||||
let button = document.querySelector('[data-button]')
|
||||
let hiddenField = document.querySelector('[data-hidden-field]')
|
||||
button.addEventListener('click', function() {
|
||||
hiddenField.style.display = "block"
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<%= render(SimpleJavascriptInteractionWithoutJsIncludedComponent.new) %>
|
|
@ -0,0 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SimpleJavascriptInteractionWithJsIncludedComponent < ViewComponent::Base
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
<button id='button' data-button>Click Me To Reveal Something Cool</button>
|
||||
<div data-hidden-field style="display: none">
|
||||
Now you see me!
|
||||
</div>
|
|
@ -0,0 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class SimpleJavascriptInteractionWithoutJsIncludedComponent < ViewComponent::Base
|
||||
end
|
|
@ -8,3 +8,13 @@
|
|||
<%= yield %>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function(event) {
|
||||
let button = document.querySelector('[data-button]')
|
||||
let hiddenField = document.querySelector('[data-hidden-field]')
|
||||
button.addEventListener('click', function() {
|
||||
hiddenField.style.display = "block"
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "test_helper"
|
||||
|
||||
class ViewComponentSystemTest < ViewComponent::SystemTestCase
|
||||
driven_by :cuprite
|
||||
|
||||
def test_simple_js_interaction_in_browser_without_layout
|
||||
with_rendered_component_path(render_inline(SimpleJavascriptInteractionWithJsIncludedComponent.new)) do |path|
|
||||
visit path
|
||||
|
||||
assert find("[data-hidden-field]", visible: false)
|
||||
find("[data-button]", text: "Click Me To Reveal Something Cool").click
|
||||
assert find("[data-hidden-field]", visible: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_simple_js_interaction_in_browser_with_layout
|
||||
with_rendered_component_path(render_inline(SimpleJavascriptInteractionWithoutJsIncludedComponent.new), layout: "application") do |path|
|
||||
visit path
|
||||
|
||||
assert find("[data-hidden-field]", visible: false)
|
||||
find("[data-button]", text: "Click Me To Reveal Something Cool").click
|
||||
assert find("[data-hidden-field]", visible: true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_component_with_params
|
||||
with_rendered_component_path(render_inline(TitleWrapperComponent.new(title: "awesome-title"))) do |path|
|
||||
visit path
|
||||
|
||||
assert find("div", text: "awesome-title")
|
||||
end
|
||||
end
|
||||
|
||||
def test_components_with_slots
|
||||
with_rendered_component_path(render_inline(SlotsV2Component.new) do |component|
|
||||
component.title do
|
||||
"This is my title!"
|
||||
end
|
||||
end) do |path|
|
||||
visit path
|
||||
|
||||
find(".title", text: "This is my title!")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -37,6 +37,19 @@ ViewComponent::Deprecation.behavior = :silence
|
|||
require File.expand_path("sandbox/config/environment.rb", __dir__)
|
||||
require "rails/test_help"
|
||||
|
||||
require "capybara/cuprite"
|
||||
|
||||
Capybara.register_driver(:cuprite) do |app|
|
||||
# Add the process_timeout option to prevent failures due to the browser
|
||||
# taking too long to start up.
|
||||
Capybara::Cuprite::Driver.new(app, {process_timeout: 60, timeout: 30})
|
||||
end
|
||||
|
||||
# Reduce extra logs produced by puma booting up
|
||||
Capybara.server = :puma, {Silent: true}
|
||||
# Increase the max wait time to appease test failures due to timeouts.
|
||||
Capybara.default_max_wait_time = 10
|
||||
|
||||
def with_config_option(option_name, new_value, config_entrypoint: Rails.application.config.view_component)
|
||||
old_value = config_entrypoint.public_send(option_name)
|
||||
config_entrypoint.public_send("#{option_name}=", new_value)
|
||||
|
|
Загрузка…
Ссылка в новой задаче