Rails viewcomponents test render? in RSpec - ruby-on-rails

Rails Viewcomponents allow you to test if a component has rendered in minitest using
refute_component_rendered but how do you do the same in RSpec?
class SometimesNotRenderedComponent < ViewComponent::Base
def initialize(my_param)
#my_param = my_param
end
def render?
# test this
end
end
it "renders nothing when..." do
render_inline(described_class.new(my_param))
# expect(page).to ... have no content
end

Let's dig a little, otherwise, the answer would be quite short.
The code for refute_component_rendered is pretty simple:
https://github.com/ViewComponent/view_component/blob/v2.78.0/lib/view_component/test_helpers.rb#L14
def refute_component_rendered
assert_no_selector("body")
end
assert_no_selector is a Capybara matcher. Negative matchers for rspec are defined dynamically with a prefix have_no_ and are not documented.
have_selector delegates to assert_selector.
https://www.rubydoc.info/gems/capybara/Capybara/RSpecMatchers#have_selector-instance_method
Which means have_no_selector delegates to assert_no_selector.
You can use either one, it's just a matter of preference:
it "does not render" do
render_inline(described_class.new(nil))
expect(page).to have_no_selector("body")
expect(page).not_to have_selector("body")
end
it "renders, but why" do
# why match on `body`? it is just how `render_inline` method works,
# https://github.com/ViewComponent/view_component/blob/v2.78.0/lib/view_component/test_helpers.rb#L56
# it assignes whatever the result of `render` to #rendered_content, like this:
#rendered_content = "i'm rendering"
# when you call `page`
# https://github.com/ViewComponent/view_component/blob/v2.78.0/lib/view_component/test_helpers.rb#L10
# it wraps #rendered_content in `Capybara::Node::Simple`
# https://www.rubydoc.info/gems/capybara/Capybara/Node/Simple
# if content is empty, there is no body
# Capybara::Node::Simple.new("").native.to_html
# # => "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">\n\n"
puts page.native.to_xhtml # to see what you're matching on
expect(page).to_not have_no_selector("body")
end
I've double checked:
$ bin/rspec spec/components/badge_component_spec.rb
BadgeComponent
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
does not render
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<body>
<p>i'm rendering</p>
</body>
</html>
renders, but why
Finished in 0.06006 seconds (files took 2.11 seconds to load)
2 examples, 0 failures

Related

How to create a Nokogiri::XML::Node from Nokogiri::XML::Builder

I need to replace a node in a document with new HTML I'm creating.
The class of the node I have to replace is:
Nokogiri::XML::Node
I create my fragment using the Nokogiri Builder:
new_node = Nokogiri::XML::Builder.new do |xml|
xml.table('border' => '1', 'cellpadding' => '1', 'cellspacing' => '1') {
xml.thead {
xml.tr {
battery_test[0..4].each do |head|
xml.th_ head["inputValue"]
end
}
}
xml.tbody {
battery_test.drop(5).each_slice(5) do |row|
xml.tr {
row.each do |item|
xml.td_ item["inputValue"]
end
}
end
}
}
end
But the class of new_node is Nokogiri::XML::Builder.
How can I replace my Nokogiri::XML::Node with the fragment I create with the builder?
You don't have to use Builder to create nodes. Nokogiri allows several ways of defining them. Your question isn't asked well as it's missing essential information, but this will get you started:
require 'nokogiri'
doc = Nokogiri::HTML(<<EOT)
<html>
<head></head>
<body>
</body>
</html>
EOT
puts doc.to_html
# >> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
# >> <html>
# >> <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>
# >> <body>
# >> </body>
# >> </html>
I can add a table using a string containing the HTML:
body = doc.at('body')
body.inner_html = "<table><tbody><tr><td>foo</td><td>bar</td></tr></tbody></table>"
puts doc.to_html
# >> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
# >> <html>
# >> <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>
# >> <body><table><tbody><tr>
# >> <td>foo</td>
# >> <td>bar</td>
# >> </tr></tbody></table></body>
# >> </html>
Modify the string generation to contain the HTML you need, let Nokogiri do the heavy lifting, and you're done. It's easier to read and maintain.
inner_html= is defined as:
inner_html=(node_or_tags)
node_or_tags means you can pass a node created using Builder, snipped from some other place in the DOM, or a string containing the markup.
Similarly:
table = Nokogiri::XML::Node.new('table', doc)
table.class # => Nokogiri::XML::Element
table.add_child('<tbody><tr><td>foo</td><td>bar</td></tr></tbody>')
body = doc.at('body')
body.inner_html = table
puts doc.to_html
# >> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
# >> <html>
# >> <head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head>
# >> <body><table><tbody><tr>
# >> <td>foo</td>
# >> <td>bar</td>
# >> </tr></tbody></table></body>
# >> </html>
Note that table is a Nokogiri::XML::Element. HTML nodes are a subclass of XML nodes so don't let that confuse you.
The tutorials are good starting points for trying anything with Nokogiri. In this case "Modifying an HTML / XML Document" is useful. Also the "Cheat sheet" is chock-full of goodness. Finally, "Questions tagged [nokogiri]" reveals all the top questions on Stack Overflow.

Generating a PDF with Sidekiq & Wicked PDF: Undefined local variable or method for worker#

I've been trying to get PDF's generated via sidekiq and wicked_pdf in a rails 5.1 app but keep getting this error:
2017-11-01T02:20:33.339Z 1780 TID-ovys2t32c GeneratePdfWorker JID-b3e9487113db23d65b179b1c INFO: start
2017-11-01T02:20:33.369Z 1780 TID-ovys2t32c GeneratePdfWorker JID-b3e9487113db23d65b179b1c INFO: fail: 0.03 sec
2017-11-01T02:20:33.371Z 1780 TID-ovys2t32c WARN: {"class":"GeneratePdfWorker","args":[2,1],"retry":false,"queue":"default","jid":"b3e9487113db23d65b179b1c","created_at":1509502833.334234,"enqueued_at":1509502833.3345}
2017-11-01T02:20:33.380Z 1780 TID-ovys2t32c WARN: NameError: undefined local variable or method `quote' for #<GeneratePdfWorker:0x007fb6d5cea070>
Did you mean? #quote
2017-11-01T02:20:33.380Z 1780 TID-ovys2t32c WARN: /Users/stefanbullivant/quottes/app/workers/generate_pdf_worker.rb:18:in `perform'
I get this error even with no locals being passed in the av.render method. Any ideas on what's causing it are appreciated.
quotes_controller.rb calling the worker
def create_pdf
#quote = Quote.find(params[:id])
GeneratePdfWorker.perform_async(#quote.id, current_account.id)
redirect_to #quote
end
Generate_pdf_worker.rb
class GeneratePdfWorker
include Sidekiq::Worker
sidekiq_options retry: false
def perform(quote_id, account_id)
#quote = Quote.find(quote_id)
#account = Account.find(account_id)
# create an instance of ActionView, so we can use the render method outside of a controller
av = ActionView::Base.new()
av.view_paths = ActionController::Base.view_paths
# need these in case your view constructs any links or references any helper methods.
av.class_eval do
include Rails.application.routes.url_helpers
include ApplicationHelper
end
pdf = av.render pdf: "Quote ##{ #quote.id } for #{ #quote.customer_name }",
file: "#{ Rails.root }/tmp/pdfs/quote_#{#quote.id}_#{#quote.customer_name}.pdf",
template: 'quotes/create_pdf.html.erb',
layout: 'layouts/quotes_pdf.html.erb',
disposition: 'attachment',
disable_javascript: true,
enable_plugins: false,
locals: {
quote: #quote,
account: #account
}
# pdf_html = av.render :template => "quotes/create_pdf.html.erb",
# :layout => "layouts/quotes_pdf.html.erb",
# :locals => {
# quote: #quote,
# account: #account
# }
# use wicked_pdf gem to create PDF from the doc HTML
quote_pdf = WickedPdf.new.pdf_from_string(pdf, :page_size => 'A4')
# save PDF to disk
pdf_path = Rails.root.join('tmp', "quote.pdf")
File.open(pdf_path, 'wb') do |file|
file << quote_pdf
en
end
end
quotes_pdf.html.erb PDF Layout
<!DOCTYPE html>
<html>
<head>
<title>Quottes</title>
<meta charset="utf-8" />
<meta name="ROBOTS" content="NOODP" />
<style>
<%= wicked_pdf_stylesheet_link_tag 'quote_pdf' -%>
</style>
</head>
<body>
<div class="container">
<%= yield %>
</div>
</body>
</html>
create_pdf.html.erb PDF View (for the sake of getting things running first, just two lines using each local)
<%= account.brand_name %>
<%= quote.customer_name %>
Any advice on getting this running is much appreciated. I have played around with simply generating plain html text in the pdf view without passing any variables and still get this error so I'm confused as to what's causing it.
I sometimes forget about this as well - It seems like it's very insistent that line 18 (which I can only assume is render) is looking up the quote local and can't find it, and #quote is defined at that time. If that is all your code, then I would presume the changes are not being picked up.
My best suggestion (which I hope works) is you need to restart sidekiq!

How to get mails correctly

I am new to Rails and I am faced with a problem:
I have to build an app for a Coworking Area. Freelancers complete a form and they can have one of these two statuses: accept or confirm.
I handle this with Enum type.
I can get their mails: Freelancer.confirm.map{ |freelancer| freelancer.email }
In Rails console:
Freelancer Load (2.8ms) SELECT "freelancers".* FROM "freelancers"
WHERE "freelancers"."status" = ? [["status", 0]] =>
["freelancer1#mail.com", "freelancer2#mail.com",
"freelancer3#mail.fr", "freelancer4#mail.com", "freelancer5#mail.fr",
"freelancer6#mail.com", "freelancer7#mail.com",
"freelancer8#mail.com", "freelancer9#mail.com",
"freelancer10#mail.com", "freelancer11#mail.com"]
But I can't use it correctly in my app:
In my app/models/freelancer.rb
class Freelancer < ApplicationRecord
enum status: [:confirm, :accept]
def send_contract_email
UserMailer.contract_email(self).deliver_now
end
end
In app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
def contract_email(freelancers)
#emails = freelancers.accept.map{|freelancer| freelancer.email}
mail(to: #emails, subject: 'Welcome to my site')
end
end
In lib/tasks/email_tasks.rake
desc 'send contract email'
task send_contract_email: :environment do
UserMailer.contract_email(freelancers).deliver_now
end
In config/environments/schedule.rb (set to 5 minutes for more convenience)
every '5 * * * *' do
rake 'send_contract_email'
end
And in app/views/user_mailer/contract_email.html.erb
<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=UTF-8' http-equiv='Content-Type' />
</head>
<body>
<h1>Hi!</h1>
</body>
</html>
When I run
rake send_contract_email
for testing everything works and checking whether the email is sent or not, it displays an error:
rake aborted! NameError: undefined local variable or method
freelancers' for main:Object
/home/gaelle/Bureau/corworking/coworking/lib/tasks/email_tasks.rake:3:in
block in '
/home/gaelle/.rvm/gems/ruby-2.4.1/gems/rake-12.1.0/exe/rake:27:in
<top (required)>'
/home/gaelle/.rvm/gems/ruby-2.4.1/bin/ruby_executable_hooks:15:in
eval'
/home/gaelle/.rvm/gems/ruby-2.4.1/bin/ruby_executable_hooks:15:in
`' Tasks: TOP => send_contract_email (See full trace by running
task with --trace)
The problem seems to be in UserMailer but I can not understand why I am wrong because I use Freelancer (with self) in the model.
Thanks for your help!
It looks like in your task, send_contract_email, you're using a local variable freelancers that hasn't been initialized. You'll need to set freelancers to be a list of freelancers in the task before calling UserMailer.contract_email, or else pass the list into the task as a parameter. Here's a nice post with information about how to pass parameters to rake tasks.

Add text just before the closing tag using Nokogiri

I'm using a Nokogiri-based helper to truncate text without breaking HTML tags:
require "rubygems"
require "nokogiri"
module TextHelper
def truncate_html(text, max_length, ellipsis = "...")
ellipsis_length = ellipsis.length
doc = Nokogiri::HTML::DocumentFragment.parse text
content_length = doc.inner_text.length
actual_length = max_length - ellipsis_length
content_length > actual_length ? doc.truncate(actual_length).inner_html + ellipsis : text.to_s
end
end
module NokogiriTruncator
module NodeWithChildren
def truncate(max_length)
return self if inner_text.length <= max_length
truncated_node = self.dup
truncated_node.children.remove
self.children.each do |node|
remaining_length = max_length - truncated_node.inner_text.length
break if remaining_length <= 0
truncated_node.add_child node.truncate(remaining_length)
end
truncated_node
end
end
module TextNode
def truncate(max_length)
Nokogiri::XML::Text.new(content[0..(max_length - 1)], parent)
end
end
end
Nokogiri::HTML::DocumentFragment.send(:include, NokogiriTruncator::NodeWithChildren)
Nokogiri::XML::Element.send(:include, NokogiriTruncator::NodeWithChildren)
Nokogiri::XML::Text.send(:include, NokogiriTruncator::TextNode)
On
content_length > actual_length ? doc.truncate(actual_length).inner_html + ellipsis : text.to_s
it appends the ellipse just after the last tag.
On my view I call
<%= truncate_html(news.parsed_body, 700, "... Read more.").html_safe %>
The issue is that the text that is being parsed is wrapped in <p></p> tags, causing the view to break:
"Lorem Ipsum</p>
... Read More"
Is it possible to append the ellipse to the last part of the last node using Nokogiri, so the final output becomes:
"Loren Ipsum... Read More</p>
Since you didn't supply any input data you get to interpolate from this:
require 'nokogiri'
doc = Nokogiri::HTML(<<EOT)
<html>
<body>
<p>foo bar baz</p>
</body>
</html>
EOT
paragraph = doc.at('p')
text = paragraph.text
text[4..-1] = '...'
paragraph.content = text
puts doc.to_html
# >> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
# >> <html>
# >> <body>
# >> <p>foo ...</p>
# >> </body>
# >> </html>
You're making it much harder than it really is. Nokogiri is smart enough to know whether we're passing markup, or simply text, and content will create a text node or an element depending on which it is.
This code simply:
Finds the p tag.
Extracts the text from it.
Replaces the text from a given point to the end with '...'.
Replaces the content of the paragraph with that text.
If you only want to append to that text it becomes even easier:
paragraph = doc.at('p')
paragraph.content = paragraph.text + ' ...Read more.'
puts doc.to_html
# >> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
# >> <html>
# >> <body>
# >> <p>foo bar baz ...Read more.</p>
# >> </body>
# >> </html>

Problems Rendering View (ActionView::MissingTemplate ... Error) in Custom Plugin

I am trying to develop a plugin for Ruby on Rails and came across problems rendering my html view. My directory structure looks like so:
File Structure
---/vendor
|---/plugins
|---/todo
|---/lib
|---/app
|---/controllers
|---todos_controller.rb
|---/models
|---todos.rb
|---/views
|---index.html.erb
|---todo_lib.rb
|---/rails
|---init.rb
In /rails/init.rb
require 'todo_lib'
In /lib/app/todo_lib.rb
%w{ models controllers views }.each do |dir|
# Include the paths:
# /Users/Me/Sites/myRailsApp/vendor/plugins/todo/lib/app/models
# /Users/Me/Sites/myRailsApp/vendor/plugins/todo/lib/app/controllers
# /Users/Me/Sites/myRailsApp/vendor/plugins/todo/lib/app/views
path = File.expand_path(File.join(File.dirname(__FILE__), 'app', dir))
# We add the above path to be included when Rails boots up
$LOAD_PATH << path
ActiveSupport::Dependencies.load_paths << path
ActiveSupport::Dependencies.load_once_paths.delete(path)
end
In todo/lib/app/controllers/todos_controller.rb
class TodosController < ActionController::Base
def index
end
end
In todo/lib/app/views/index.html.erb
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"[url]http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd[/url]">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Todos:</title>
</head>
<body>
<p style="color: green" id="flash_notice"><%= flash[:notice] %></p>
<h1>Listing Todos</h1>
</body>
</html>
In /myRailsApp/config/routes.rb
ActionController::Routing::Routes.draw do |map|
# The priority is based upon order of creation: first created -> highest priority.
map.resources :todos
...
The error I get is the following:
Template is missing
Missing template todos/index.erb in view path app/views
Can anyone give me a hand up and tell me what am I doing wrong here that is causing my index.html.erb file to not render? Much appreciated!
EDIT:
I have already tried the following without success:
In /todo/lib/app/controllers/todos_controller.rb
def index
respond_to do |format|
format.html # index.html.erb
end
end
EDIT:
hakunin solved this problem. Here's the solution.
He says that I'm building a Rails engine plugin (I had no idea I was doing this), and it requires a different directory structure, one that appears like so:
File Structure
---/vendor
|---/plugins
|---/todo
|---/lib
|---/app
|---/controllers
|---todos_controller.rb
|---/models
|---todos.rb
|---/views
|---/todos
|---index.html.erb
|---todo_lib.rb
|---/rails
|---init.rb
This required the following changes:
In todo/lib/todo_lib.rb
%w{ models controllers views }.each do |dir|
# Include the paths:
# /Users/Me/Sites/myRailsApp/vendor/plugins/todo/app/models
# /Users/Me/Sites/myRailsApp/vendor/plugins/todo/app/controllers
# /Users/Me/Sites/myRailsApp/vendor/plugins/todo/app/views
path = File.expand_path(File.join(File.dirname(__FILE__), '../app', dir))
# We add the above path to be included when Rails boots up
$LOAD_PATH << path
ActiveSupport::Dependencies.load_paths << path
ActiveSupport::Dependencies.load_once_paths.delete(path)
end
The change made above is in the line: path = File.expand_path(File.join(File.dirname(FILE), '../app', dir)). [Ignore the boldened 'FILE', this is an issue with the website].
Running script/server will render the index.html.erb page under todo/app/views/todos.
Looks like you want to build an "engine" plugin. Create "app" and "config" dirs in the root of your plugin dir (not under /lib). You can use app/views/ and app/controllers in your plugin as if it was a full featured Rails app. In config/routes.rb you should declare routes introduced by your engine.
See http://github.com/neerajdotname/admin_data for a decent example of what engine looks like.

Resources