Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add render_preview test helper #1347

Merged
merged 39 commits into from
May 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
36f5acd
clean up previews docs
joelhawksley May 3, 2022
c6a8578
RSpec not rspec
joelhawksley May 3, 2022
2ae1f61
always find preview for action
joelhawksley May 3, 2022
598f54c
rename @rendered_component to @rendered_page
joelhawksley May 3, 2022
11aa0cf
Introduce `render_preview` test helper
joelhawksley May 3, 2022
b425fa6
rename rendered_page to rendered_content
joelhawksley May 3, 2022
d425ba2
rename benchmark
joelhawksley May 4, 2022
929016b
copy edits
joelhawksley May 4, 2022
dbce872
Merge branch 'main' into render_preview
joelhawksley May 9, 2022
ab62dbf
re-add lookbook note
joelhawksley May 9, 2022
464d921
Update docs/CHANGELOG.md
joelhawksley May 10, 2022
1dddf60
Update docs/CHANGELOG.md
joelhawksley May 10, 2022
a639265
Merge branch 'main' into render_preview
joelhawksley May 10, 2022
f913810
update depa
joelhawksley May 11, 2022
bfa77c0
add note to changelog
joelhawksley May 11, 2022
8d93e6f
render_preview is opt-in
joelhawksley May 11, 2022
06e7bac
Merge branch 'main' into render_preview
joelhawksley May 11, 2022
9bdadfe
fix broken reference in benchmark build
joelhawksley May 11, 2022
7b8a3fe
always load descendants
joelhawksley May 11, 2022
5de1d4d
add Preview.load_all/load_all!
joelhawksley May 11, 2022
db91b42
Merge branch 'main' into render_preview
joelhawksley May 11, 2022
1a3eb4c
how about this?
joelhawksley May 11, 2022
09f42de
remove duplicate before_action
joelhawksley May 11, 2022
d16f1d9
Ruby 3 changed error formatting!
joelhawksley May 11, 2022
86455ff
don't assert with period
joelhawksley May 11, 2022
ebaea4a
mdlint
joelhawksley May 11, 2022
3fbc56a
Merge branch 'main' into render_preview
joelhawksley May 13, 2022
7cda925
rename ivar
joelhawksley May 18, 2022
20e5e4a
always load all previews
joelhawksley May 25, 2022
45daf39
try this
joelhawksley May 25, 2022
8fa0fc0
explicitly load previews
joelhawksley May 26, 2022
338c3b7
Merge branch 'main' into render_preview
joelhawksley May 26, 2022
f89176c
Merge branch 'main' into render_preview
joelhawksley May 26, 2022
95b115f
one more try
joelhawksley May 26, 2022
ffc855f
this one?
joelhawksley May 26, 2022
d34254c
load in engine?
joelhawksley May 26, 2022
5ed25ff
uncomment load_previews call
joelhawksley May 31, 2022
359ea37
Merge branch 'main' into render_preview
joelhawksley May 31, 2022
780b0ad
remove call in engine
joelhawksley May 31, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
run: |
bundle config path vendor/bundle
bundle update
bundle exec rake benchmark
bundle exec rake partial_benchmark
bundle exec rake translatable_benchmark
bundle exec rake slotable_benchmark
test:
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,9 @@ DEPENDENCIES
haml (~> 5)
jbuilder (~> 2)
minitest (= 5.6.0)
net-imap
net-pop
net-smtp
pry (~> 0.13)
rails (~> 7.0.0)
rake (~> 13.0)
Expand Down
4 changes: 2 additions & 2 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ Rake::TestTask.new(:test) do |t|
end

desc "Runs benchmarks against components"
task :benchmark do
ruby "./performance/benchmark.rb"
task :partial_benchmark do
ruby "./performance/partial_benchmark.rb"
end

desc "Runs benchmarks against component content area/ slot implementations"
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/concerns/view_component/preview_actions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ module PreviewActions
prepend_view_path File.expand_path("../../../views", __dir__)

around_action :set_locale, only: :previews
before_action :find_preview, only: :previews
before_action :require_local!, unless: :show_previews?

if respond_to?(:content_security_policy)
Expand All @@ -23,6 +22,8 @@ def index
end

def previews
find_preview
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious, why was this change necessary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. Since we're calling previews directly from render_preview, before_action isn't called. In fact, I think we could inline find_preview here!


if params[:path] == @preview.preview_name
@page_title = "Component Previews for #{@preview.preview_name}"
render "view_components/previews", **determine_layout
Expand Down
4 changes: 4 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ title: Changelog

## main

* Introduce experimental `render_preview` test helper. Note: `@rendered_component` in `TestHelpers` has been renamed to `@rendered_content`.

*Joel Hawksley*

* Move framework tests into sandbox application.

*Joel Hawksley*
Expand Down
65 changes: 41 additions & 24 deletions docs/guide/previews.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@ parent: Guide

# Previews

`ViewComponent::Preview`, like `ActionMailer::Preview`, provides a quick way to preview components in isolation.

_For a more interactive experience, consider using [ViewComponent::Storybook](https://github.com/jonspalmer/view_component_storybook) or [Lookbook](https://github.com/allmarkedup/lookbook)._
Spone marked this conversation as resolved.
Show resolved Hide resolved

Define a `ViewComponent::Preview`:
`ViewComponent::Preview`, like `ActionMailer::Preview`, provides a quick way to preview components in isolation:

```ruby
# test/components/previews/example_component_preview.rb
Expand All @@ -19,10 +15,6 @@ class ExampleComponentPreview < ViewComponent::Preview
render(ExampleComponent.new(title: "Example component default"))
end

def with_long_title
render(ExampleComponent.new(title: "This is a really long title to see how the component renders this"))
end

def with_content_block
render(ExampleComponent.new(title: "This component accepts a block of content")) do
tag.div do
Expand All @@ -35,11 +27,30 @@ end

Then access the resulting previews at:

* <http://localhost:3000/rails/view_components/example_component/with_default_title>
* <http://localhost:3000/rails/view_components/example_component/with_long_title>
* <http://localhost:3000/rails/view_components/example_component/with_content_block>
* `/rails/view_components/example_component/with_default_title`
* `/rails/view_components/example_component/with_content_block`

_For a more interactive experience, consider using [Lookbook](https://github.com/allmarkedup/lookbook) or [ViewComponent::Storybook](https://github.com/jonspalmer/view_component_storybook)._

## (Experimental) Previews as test cases

Use `render_preview(name)` to render previews in ViewComponent unit tests:

```ruby
class ExampleComponentTest < ViewComponent::TestCase
include ViewComponent::RenderPreviewHelper

def test_render_preview
render_preview(:with_default_title)

It's also possible to set dynamic values from the params by setting them as arguments:
assert_text("Example component default")
end
end
```

## Passing parameters

Set dynamic values from URL parameters by setting them as arguments:

```ruby
# test/components/previews/example_component_preview.rb
Expand All @@ -50,13 +61,17 @@ class ExampleComponentPreview < ViewComponent::Preview
end
```

Which enables passing in a value with <http://localhost:3000/rails/view_components/example_component/with_dynamic_title?title=Custom+title>.
Then pass in a value: `/rails/view_components/example_component/with_dynamic_title?title=Custom+title`.

## Helpers

The `ViewComponent::Preview` base class includes
[`ActionView::Helpers::TagHelper`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html), which provides the [`tag`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-tag)
and [`content_tag`](https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-content_tag) view helper methods.

Previews use the application layout by default, but can use a specific layout with the `layout` option:
## Layouts

Previews render with the application layout by default, but can use a specific layout with the `layout` option:

```ruby
# test/components/previews/example_component_preview.rb
Expand All @@ -67,30 +82,32 @@ class ExampleComponentPreview < ViewComponent::Preview
end
```

To set a custom layout for previews and the previews index page, set: `default_preview_layout`:
To set a custom layout for individual previews and the previews index page, set: `default_preview_layout`:

```ruby
# config/application.rb
# Set the default layout to app/views/layouts/component_preview.html.erb
config.view_component.default_preview_layout = "component_preview"
```

## Preview paths

Preview classes live in `test/components/previews`, which can be configured using the `preview_paths` option:

```ruby
# config/application.rb
config.view_component.preview_paths << "#{Rails.root}/lib/component_previews"
```

Previews are served from <http://localhost:3000/rails/view_components> by default. To use a different endpoint, set the `preview_route` option:
## Previews route

Previews are served from `/rails/view_components` by default. To use a different endpoint, set the `preview_route` option:

```ruby
# config/application.rb
config.view_component.preview_route = "/previews"
```

This example will make the previews available from <http://localhost:3000/previews>.

## Preview templates

Given a preview `test/components/previews/cell_component_preview.rb`, template files can be defined at `test/components/previews/cell_component_preview/`:
Expand Down Expand Up @@ -140,11 +157,11 @@ class CellComponentPreview < ViewComponent::Preview
end
```

Which enables passing in a value with <http://localhost:3000/rails/view_components/cell_component/default?title=Custom+title&subtitle=Another+subtitle>.
Which enables passing in a value: `/rails/view_components/cell_component/default?title=Custom+title&subtitle=Another+subtitle`.

## Configuring preview controller

Previews can be extended to allow users to add authentication, authorization, before actions, or anything that the end user would need to meet their needs using the `preview_controller` option:
Extend previews to add authentication, authorization, before actions, etc. using the `preview_controller` option:

```ruby
# config/application.rb
Expand All @@ -170,11 +187,11 @@ Source previews are disabled by default. To enable or disable source previews, u
config.view_component.show_previews_source = true
```

To render a source preview in a different place, use the view helper `preview_source` from within your preview template or preview layout.
To render the source preview in a different location, use the view helper `preview_source` from within the preview template or preview layout.

## Using with rspec
## Use with RSpec

When using previews with rspec, replace `test/components` with `spec/components` and update `preview_paths`:
When using previews with RSpec, replace `test/components` with `spec/components` and update `preview_paths`:

```ruby
# config/application.rb
Expand Down
16 changes: 16 additions & 0 deletions docs/guide/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ end

_Note: `assert_selector` only matches on visible elements by default. To match on elements regardless of visibility, add `visible: false`. See the [Capybara documentation](https://rubydoc.info/github/jnicklas/capybara/Capybara/Node/Matchers) for more details._

## (Experimental) Previews as test cases

Use `render_preview(name)` to render previews in ViewComponent unit tests:

```ruby
class ExampleComponentTest < ViewComponent::TestCase
include ViewComponent::RenderPreviewHelper

def test_render_preview
render_preview(:with_default_title)

assert_text("Example component default")
end
end
```

## Best practices

Prefer testing the rendered output over individual methods:
Expand Down
7 changes: 4 additions & 3 deletions lib/view_component/preview.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def render_with_template(template: nil, locals: {})
class << self
# Returns all component preview classes.
def all
load_previews if descendants.empty?
load_previews

descendants
end

Expand Down Expand Up @@ -98,14 +99,14 @@ def preview_source(example)
source[1...(source.size - 1)].join("\n")
end

private

def load_previews
Array(preview_paths).each do |preview_path|
Dir["#{preview_path}/**/*_preview.rb"].sort.each { |file| require_dependency file }
end
end

private

def preview_paths
Base.preview_paths
end
Expand Down
40 changes: 40 additions & 0 deletions lib/view_component/render_preview_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module ViewComponent
module RenderPreviewHelper
# Render a preview inline. Internally sets `page` to be a `Capybara::Node::Simple`,
# allowing for Capybara assertions to be used:
#
# ```ruby
# render_preview(:default)
# assert_text("Hello, World!")
# ```
#
# Note: `#rendered_preview` expects a preview to be defined with the same class
# name as the calling test, but with `Test` replaced with `Preview`:
#
# MyComponentTest -> MyComponentPreview etc.
#
# @param preview [String] The name of the preview to be rendered.
# @return [Nokogiri::HTML]
def render_preview(name)
begin
preview_klass = self.class.name.gsub("Test", "Preview")
preview_klass = preview_klass.constantize
rescue NameError
raise NameError.new(
"`render_preview` expected to find #{preview_klass}, but it does not exist."
)
end

previews_controller = build_controller(ViewComponent::Base.preview_controller.constantize)
previews_controller.request.params[:path] = "#{preview_klass.preview_name}/#{name}"
previews_controller.response = ActionDispatch::Response.new
result = previews_controller.previews

@rendered_content = result

Nokogiri::HTML.fragment(@rendered_content)
end
end
end
12 changes: 7 additions & 5 deletions lib/view_component/test_helpers.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# frozen_string_literal: true

require "view_component/render_preview_helper"

module ViewComponent
module TestHelpers
begin
require "capybara/minitest"
include Capybara::Minitest::Assertions

def page
Capybara::Node::Simple.new(@rendered_component)
Capybara::Node::Simple.new(@rendered_content)
end

def refute_component_rendered
Expand Down Expand Up @@ -41,14 +43,14 @@ def refute_component_rendered
# @param component [ViewComponent::Base, ViewComponent::Collection] The instance of the component to be rendered.
# @return [Nokogiri::HTML]
def render_inline(component, **args, &block)
@rendered_component =
@rendered_content =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super minor, but I wonder if this is being used directly by any libraries/apps and if renaming it would break anything.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do wonder that too. I'll add a note to the changelog.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This indeed is used by lots of people! #1372 and #1373 offer two fixes.

I wonder if we should update the documentation to more explicitly direct people towards writing expectations that use page if they are using RSpec and Capybara?

expect(page).to have_link("Home page", href: root_path)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related issue: #1375

if Rails.version.to_f >= 6.1
controller.view_context.render(component, args, &block)
else
controller.view_context.render_component(component, &block)
end

Nokogiri::HTML.fragment(@rendered_component)
Nokogiri::HTML.fragment(@rendered_content)
end

# Execute the given block in the view context. Internally sets `page` to be a
Expand All @@ -62,8 +64,8 @@ def render_inline(component, **args, &block)
# assert_text("Hello, World!")
# ```
def render_in_view_context(&block)
@rendered_component = controller.view_context.instance_exec(&block)
Nokogiri::HTML.fragment(@rendered_component)
@rendered_content = controller.view_context.instance_exec(&block)
Nokogiri::HTML.fragment(@rendered_content)
end

# @private
Expand Down
File renamed without changes.
17 changes: 17 additions & 0 deletions test/sandbox/test/components/my_component_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

require "test_helper"

class MyComponentTest < ViewComponent::TestCase
include ViewComponent::RenderPreviewHelper

def setup
ViewComponent::Preview.load_previews
end

def test_render_preview
render_preview(:default)

assert_selector("div", text: "hello,world!")
end
end
18 changes: 18 additions & 0 deletions test/sandbox/test/components/without_preview_component_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

require "test_helper"

class WithoutPreviewComponentTest < ViewComponent::TestCase
include ViewComponent::RenderPreviewHelper

def test_render_preview
error = assert_raises NameError do
render_preview(:default)
end

assert_equal(
error.message.split(".")[0],
"`render_preview` expected to find WithoutPreviewComponentPreview, but it does not exist"
)
end
end
4 changes: 4 additions & 0 deletions test/sandbox/test/integration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
require "test_helper"

class IntegrationTest < ActionDispatch::IntegrationTest
def setup
ViewComponent::Preview.load_previews
end

def test_rendering_component_in_a_view
get "/"
assert_response :success
Expand Down
2 changes: 1 addition & 1 deletion test/sandbox/test/rendering_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def test_render_inline_returns_nokogiri_fragment
def test_render_inline_sets_rendered_component
render_inline(MyComponent.new)

assert_includes rendered_component, "hello,world!"
assert_includes @rendered_content, "hello,world!"
end

def test_child_component
Expand Down