Skip to content

Commit

Permalink
add render_preview test helper (#1347)
Browse files Browse the repository at this point in the history
* clean up previews docs

* RSpec not rspec

* always find preview for action

* rename @rendered_component to @rendered_page

* Introduce `render_preview` test helper

* rename rendered_page to rendered_content

* rename benchmark

* copy edits

* re-add lookbook note

* Update docs/CHANGELOG.md

Co-authored-by: Blake Williams <blakewilliams@github.com>

* Update docs/CHANGELOG.md

Co-authored-by: Blake Williams <blakewilliams@github.com>

* update depa

* add note to changelog

* render_preview is opt-in

* fix broken reference in benchmark build

* always load descendants

* add Preview.load_all/load_all!

* how about this?

* remove duplicate before_action

* Ruby 3 changed error formatting!

* don't assert with period

* mdlint

* rename ivar

* always load all previews

* try this

* explicitly load previews

* one more try

* this one?

* load in engine?

* uncomment load_previews call

* remove call in engine

Co-authored-by: Blake Williams <blakewilliams@github.com>
  • Loading branch information
joelhawksley and BlakeWilliams authored May 31, 2022
1 parent 2273e01 commit 4bceecf
Show file tree
Hide file tree
Showing 15 changed files with 160 additions and 37 deletions.
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

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)._

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 =
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

0 comments on commit 4bceecf

Please sign in to comment.