roadie 2.4.3 → 3.0.0.pre1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +9 -14
  4. data/.yardopts +1 -1
  5. data/Changelog.md +22 -10
  6. data/Gemfile +3 -0
  7. data/Guardfile +11 -1
  8. data/README.md +165 -163
  9. data/Rakefile +2 -19
  10. data/lib/roadie.rb +14 -69
  11. data/lib/roadie/asset_provider.rb +7 -58
  12. data/lib/roadie/asset_scanner.rb +92 -0
  13. data/lib/roadie/document.rb +103 -0
  14. data/lib/roadie/errors.rb +57 -0
  15. data/lib/roadie/filesystem_provider.rb +21 -62
  16. data/lib/roadie/inliner.rb +71 -218
  17. data/lib/roadie/markup_improver.rb +88 -0
  18. data/lib/roadie/null_provider.rb +13 -0
  19. data/lib/roadie/null_url_rewriter.rb +12 -0
  20. data/lib/roadie/provider_list.rb +67 -0
  21. data/lib/roadie/rspec.rb +1 -0
  22. data/lib/roadie/rspec/asset_provider.rb +49 -0
  23. data/lib/roadie/selector.rb +42 -18
  24. data/lib/roadie/style_block.rb +33 -0
  25. data/lib/roadie/style_properties.rb +29 -0
  26. data/lib/roadie/style_property.rb +93 -0
  27. data/lib/roadie/stylesheet.rb +65 -0
  28. data/lib/roadie/url_generator.rb +126 -0
  29. data/lib/roadie/url_rewriter.rb +84 -0
  30. data/lib/roadie/version.rb +1 -1
  31. data/roadie.gemspec +6 -10
  32. data/spec/fixtures/big_em.css +1 -0
  33. data/spec/fixtures/stylesheets/green.css +1 -0
  34. data/spec/integration_spec.rb +125 -95
  35. data/spec/lib/roadie/asset_scanner_spec.rb +153 -0
  36. data/spec/lib/roadie/css_not_found_spec.rb +16 -0
  37. data/spec/lib/roadie/document_spec.rb +123 -0
  38. data/spec/lib/roadie/filesystem_provider_spec.rb +25 -72
  39. data/spec/lib/roadie/inliner_spec.rb +105 -537
  40. data/spec/lib/roadie/markup_improver_spec.rb +78 -0
  41. data/spec/lib/roadie/null_provider_spec.rb +21 -0
  42. data/spec/lib/roadie/null_url_rewriter_spec.rb +19 -0
  43. data/spec/lib/roadie/provider_list_spec.rb +81 -0
  44. data/spec/lib/roadie/selector_spec.rb +7 -5
  45. data/spec/lib/roadie/style_block_spec.rb +35 -0
  46. data/spec/lib/roadie/style_properties_spec.rb +61 -0
  47. data/spec/lib/roadie/style_property_spec.rb +82 -0
  48. data/spec/lib/roadie/stylesheet_spec.rb +41 -0
  49. data/spec/lib/roadie/test_provider_spec.rb +29 -0
  50. data/spec/lib/roadie/url_generator_spec.rb +120 -0
  51. data/spec/lib/roadie/url_rewriter_spec.rb +79 -0
  52. data/spec/shared_examples/asset_provider.rb +11 -0
  53. data/spec/shared_examples/url_rewriter.rb +23 -0
  54. data/spec/spec_helper.rb +5 -60
  55. data/spec/support/have_node_matcher.rb +2 -2
  56. data/spec/support/have_selector_matcher.rb +1 -1
  57. data/spec/support/have_styling_matcher.rb +48 -14
  58. data/spec/support/test_provider.rb +13 -0
  59. metadata +73 -177
  60. data/Appraisals +0 -15
  61. data/gemfiles/rails_3.0.gemfile +0 -7
  62. data/gemfiles/rails_3.0.gemfile.lock +0 -123
  63. data/gemfiles/rails_3.1.gemfile +0 -7
  64. data/gemfiles/rails_3.1.gemfile.lock +0 -126
  65. data/gemfiles/rails_3.2.gemfile +0 -7
  66. data/gemfiles/rails_3.2.gemfile.lock +0 -124
  67. data/gemfiles/rails_4.0.gemfile +0 -7
  68. data/gemfiles/rails_4.0.gemfile.lock +0 -119
  69. data/lib/roadie/action_mailer_extensions.rb +0 -95
  70. data/lib/roadie/asset_pipeline_provider.rb +0 -28
  71. data/lib/roadie/css_file_not_found.rb +0 -22
  72. data/lib/roadie/railtie.rb +0 -39
  73. data/lib/roadie/style_declaration.rb +0 -42
  74. data/spec/fixtures/app/assets/stylesheets/integration.css +0 -10
  75. data/spec/fixtures/public/stylesheets/integration.css +0 -10
  76. data/spec/fixtures/views/integration_mailer/marketing.html.erb +0 -2
  77. data/spec/fixtures/views/integration_mailer/notification.html.erb +0 -8
  78. data/spec/fixtures/views/integration_mailer/notification.text.erb +0 -6
  79. data/spec/lib/roadie/action_mailer_extensions_spec.rb +0 -227
  80. data/spec/lib/roadie/asset_pipeline_provider_spec.rb +0 -65
  81. data/spec/lib/roadie/css_file_not_found_spec.rb +0 -29
  82. data/spec/lib/roadie/style_declaration_spec.rb +0 -49
  83. data/spec/lib/roadie_spec.rb +0 -101
  84. data/spec/shared_examples/asset_provider_examples.rb +0 -11
  85. data/spec/support/anonymous_mailer.rb +0 -21
  86. data/spec/support/change_url_options.rb +0 -5
  87. data/spec/support/parse_styling.rb +0 -25
@@ -0,0 +1,126 @@
1
+ require 'set'
2
+
3
+ module Roadie
4
+ # @api private
5
+ # Class that handles URL generation
6
+ #
7
+ # URL generation is all about converting relative URLs into absolute URLS
8
+ # according to the given options. It is written such as absolute URLs will
9
+ # get passed right through, so all URLs could be passed through here.
10
+ class UrlGenerator
11
+ attr_reader :url_options
12
+
13
+ # Create a new instance with the given URL options.
14
+ #
15
+ # Initializing without a host setting raises an error, as do unknown keys.
16
+ #
17
+ # @param [Hash] url_options
18
+ # @option url_options [String] :host (required)
19
+ # @option url_options [String, Integer] :port
20
+ # @option url_options [String] :path root path
21
+ # @option url_options [String] :scheme URL scheme ("http" is default)
22
+ # @option url_options [String] :protocol alias for :scheme
23
+ def initialize(url_options)
24
+ raise ArgumentError, "No URL options were specified" unless url_options
25
+ raise ArgumentError, "No :host was specified; options are: #{url_options.inspect}" unless url_options[:host]
26
+ validate_options url_options
27
+
28
+ @url_options = url_options
29
+ @root_uri = build_root_uri
30
+ end
31
+
32
+ # Generate an absolute URL from a relative URL.
33
+ #
34
+ # If the passed path is already an absolute URL, it will be returned as-is.
35
+ # If passed a blank path, the "root URL" will be returned. The root URL is
36
+ # the URL that the {#url_options} would generate by themselves.
37
+ #
38
+ # An optional base can be specified. The base is another relative path from
39
+ # the root that specifies an "offset" from which the path was found in. A
40
+ # common use-case is to convert a relative path found in a stylesheet which
41
+ # resides in a subdirectory.
42
+ #
43
+ # @example Normal conversions
44
+ # generator = Roadie::UrlGenerator.new host: "foo.com", scheme: "https"
45
+ # generator.generate_url("bar.html") # => "https://foo.com/bar.html"
46
+ # generator.generate_url("/bar.html") # => "https://foo.com/bar.html"
47
+ # generator.generate_url("") # => "https://foo.com"
48
+ #
49
+ # @example Conversions with a base
50
+ # generator = Roadie::UrlGenerator.new host: "foo.com", scheme: "https"
51
+ # generator.generate_url("../images/logo.png", "/css") # => "https://foo.com/images/logo.png"
52
+ # generator.generate_url("../images/logo.png", "/assets/css") # => "https://foo.com/assets/images/logo.png"
53
+ #
54
+ # @param [String] base The base which the relative path comes from
55
+ # @return [String] an absolute URL
56
+ def generate_url(path, base = "/")
57
+ return root_uri.to_s if path.nil? or path.empty?
58
+ return path if path_is_absolute?(path)
59
+
60
+ combine_segments(root_uri, base, path).to_s
61
+ end
62
+
63
+ private
64
+ attr_reader :root_uri
65
+
66
+ def build_root_uri
67
+ path = make_absolute url_options[:path]
68
+ port = parse_port url_options[:port]
69
+ scheme = normalize_scheme(url_options[:scheme] || url_options[:protocol])
70
+ URI::Generic.build(scheme: scheme, host: url_options[:host], port: port, path: path)
71
+ end
72
+
73
+ def combine_segments(root, base, path)
74
+ new_path = apply_base(base, path)
75
+ if root.path
76
+ new_path = File.join(root.path, new_path)
77
+ end
78
+ root.merge(new_path)
79
+ end
80
+
81
+ def apply_base(base, path)
82
+ if path[0] == "/"
83
+ path
84
+ else
85
+ File.join(base, path)
86
+ end
87
+ end
88
+
89
+ # Strip :// from any scheme, if present
90
+ def normalize_scheme(scheme)
91
+ return 'http' unless scheme
92
+ scheme.to_s[/^\w+/]
93
+ end
94
+
95
+ def parse_port(port)
96
+ (port ? port.to_i : port)
97
+ end
98
+
99
+ def make_absolute(path)
100
+ if path.nil? || path[0] == "/"
101
+ path
102
+ else
103
+ "/#{path}"
104
+ end
105
+ end
106
+
107
+ def path_is_absolute?(path)
108
+ not parse_path(path).relative?
109
+ end
110
+
111
+ def parse_path(path)
112
+ URI.parse(path)
113
+ rescue URI::InvalidURIError => error
114
+ raise InvalidUrlPath.new(path, error)
115
+ end
116
+
117
+ VALID_OPTIONS = Set[:host, :port, :path, :protocol, :scheme].freeze
118
+
119
+ def validate_options(options)
120
+ keys = Set.new(options.keys)
121
+ unless keys.subset? VALID_OPTIONS
122
+ raise ArgumentError, "Passed invalid options: #{(keys - VALID_OPTIONS).to_a}, valid options are: #{VALID_OPTIONS.to_a}"
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,84 @@
1
+ module Roadie
2
+ # @api private
3
+ #
4
+ # Class that rewrites URLs in the DOM.
5
+ class UrlRewriter
6
+ # @param [UrlGenerator] generator
7
+ def initialize(generator)
8
+ @generator = generator
9
+ end
10
+
11
+ # Mutates the passed DOM tree, rewriting certain element's attributes.
12
+ #
13
+ # This will make all a[href] and img[src] into absolute URLs, as well as
14
+ # all "url()" directives inside style-attributes.
15
+ #
16
+ # [nil] is returned so no one can misunderstand that this method mutates
17
+ # the passed instance.
18
+ #
19
+ # @param [Nokogiri::HTML::Document] dom
20
+ # @return [nil] DOM tree is mutated
21
+ def transform_dom(dom)
22
+ # Use only a single loop to do this
23
+ dom.css("a[href], img[src], *[style]").each do |element|
24
+ transform_element_style element if element.has_attribute?('style')
25
+ transform_element element
26
+ end
27
+ nil
28
+ end
29
+
30
+ # Mutates passed CSS, rewriting url() directives.
31
+ #
32
+ # This will make all URLs inside url() absolute.
33
+ #
34
+ # [nil] is returned so no one can misunderstand that this method mutates
35
+ # the passed string.
36
+ #
37
+ # @param [String] css the css to mutate
38
+ # @return [nil] css is mutated
39
+ def transform_css(css)
40
+ css.gsub!(CSS_URL_REGEXP) do
41
+ matches = Regexp.last_match
42
+ "url(#{matches[:quote]}#{generate_url(matches[:url])}#{matches[:quote]})"
43
+ end
44
+ end
45
+
46
+ private
47
+ def generate_url(*args) @generator.generate_url(*args) end
48
+
49
+ # Regexp matching all the url() declarations in CSS
50
+ #
51
+ # It matches without any quotes and with both single and double quotes
52
+ # inside the parenthesis. There's much room for improvement, of course.
53
+ CSS_URL_REGEXP = %r{
54
+ url\(
55
+ (?<quote>
56
+ (?:["']|%22)? # Optional opening quote
57
+ )
58
+ (?<url> # The URL.
59
+ # We match URLs with parenthesis inside it here,
60
+ # so url(foo(bar)baz) will match correctly.
61
+ [^(]* # Text leading up to before opening parens
62
+ (?:\([^)]*\))* # Texts containing parens pairs
63
+ [^(]+ # Texts without parens - required
64
+ )
65
+ \k'quote' # Closing quote
66
+ \)
67
+ }x
68
+
69
+ def transform_element(element)
70
+ case element.name
71
+ when "a" then element["href"] = generate_url element["href"]
72
+ when "img" then element["src"] = generate_url element["src"]
73
+ end
74
+ end
75
+
76
+ def transform_element_style(element)
77
+ # We need to use a setter for Nokogiri to detect the string mutation.
78
+ # If nokogiri used "dumber" data structures, this would all be redundant.
79
+ css = element["style"]
80
+ transform_css css
81
+ element["style"] = css
82
+ end
83
+ end
84
+ end
@@ -1,3 +1,3 @@
1
1
  module Roadie
2
- VERSION = '2.4.3'
2
+ VERSION = '3.0.0.pre1'
3
3
  end
data/roadie.gemspec CHANGED
@@ -11,20 +11,16 @@ Gem::Specification.new do |s|
11
11
  s.authors = ['Magnus Bergmark']
12
12
  s.email = ['magnus.bergmark@gmail.com']
13
13
  s.homepage = 'http://github.com/Mange/roadie'
14
- s.summary = %q{Making HTML emails comfortable for the Rails rockstars}
15
- s.description = %q{Roadie tries to make sending HTML emails a little less painful in Rails 3 by inlining stylesheets and rewrite relative URLs for you.}
14
+ s.summary = %q{Making HTML emails comfortable for the Ruby rockstars}
15
+ s.description = %q{Roadie tries to make sending HTML emails a little less painful by inlining stylesheets and rewriting relative URLs for you.}
16
+ s.license = "MIT"
16
17
 
17
- s.add_dependency 'nokogiri', RUBY_VERSION < '1.9.3' ? ['> 1.5.0', '< 1.6.0'] : '> 1.5.0'
18
+ s.required_ruby_version = ">= 1.9"
19
+
20
+ s.add_dependency 'nokogiri', '~> 1.6.0'
18
21
  s.add_dependency 'css_parser', '~> 1.3.4'
19
- s.add_dependency 'actionmailer', '> 3.0.0', '< 5.0.0'
20
- s.add_dependency 'sprockets'
21
22
 
22
- s.add_development_dependency 'rake'
23
- s.add_development_dependency 'rails'
24
23
  s.add_development_dependency 'rspec'
25
- s.add_development_dependency 'rspec-rails'
26
-
27
- s.add_development_dependency 'appraisal'
28
24
 
29
25
  s.extra_rdoc_files = %w[README.md Changelog.md]
30
26
  s.require_paths = %w[lib]
@@ -0,0 +1 @@
1
+ em { font-size: 200%; }
@@ -0,0 +1 @@
1
+ body { color: green; }
@@ -1,110 +1,140 @@
1
1
  require 'spec_helper'
2
2
 
3
- module Roadie
4
- shared_examples "roadie integration" do
5
- mailer = Class.new(AnonymousMailer) do
6
- default :css => 'integration', :from => 'john@example.com'
7
- append_view_path FIXTURES_PATH.join('views')
8
-
9
- # Needed for correct path lookup
10
- self.mailer_name = "integration_mailer"
11
-
12
- def notification(to, reason)
13
- @reason = reason
14
- mail(:subject => 'Notification for you', :to => to) { |format| format.html; format.text }
15
- end
16
-
17
- def marketing(to)
18
- headers('X-Spam' => 'No way! Trust us!')
19
- mail(:subject => 'Buy cheap v1agra', :to => to)
20
- end
21
-
22
- def url_options
23
- # This allows apps to calculate any options on a per-email basis
24
- super.merge(:protocol => 'https')
25
- end
26
- end
27
-
28
- def parse_html_in_email(mail)
29
- Nokogiri::HTML.parse mail.html_part.body.decoded
30
- end
31
-
32
- before(:each) do
33
- change_default_url_options(:host => 'example.app.org')
34
- mailer.delivery_method = :test
35
- end
36
-
37
- it "inlines styles for an email" do
38
- email = mailer.notification('doe@example.com', 'your quota limit has been reached')
39
-
40
- email.to.should == ['doe@example.com']
41
- email.from.should == ['john@example.com']
42
- email.should have(2).parts
43
-
44
- email.text_part.body.decoded.should_not match(/<.*>/)
45
-
46
- html = email.html_part.body.decoded
47
- html.should include '<!DOCTYPE'
48
- html.should include '<head'
49
-
50
- document = parse_html_in_email(email)
51
- document.should have_selector('body #message h1')
52
- document.should have_styling('background' => 'url(https://example.app.org/images/dots.png) repeat-x').at_selector('body')
53
- document.should have_selector('strong[contains("quota")]')
54
-
55
- # If we deliver mails we can catch weird problems with headers being invalid
56
- email.deliver
57
- end
58
-
59
- it "does not add headers for the roadie options" do
60
- email = mailer.notification('doe@example.com', 'no berries left in chest')
61
- email.header.fields.map(&:name).should_not include('css')
62
- end
63
-
64
- it "keeps custom headers in place" do
65
- email = mailer.marketing('everyone@inter.net')
66
- email.header['X-Spam'].should be_present
67
- end
3
+ describe "Roadie functionality" do
4
+ def parse_html(html)
5
+ Nokogiri::HTML.parse(html)
6
+ end
68
7
 
69
- it "applies CSS3 styles" do
70
- email = mailer.notification('doe@example.com', 'your quota limit has been reached')
71
- document = parse_html_in_email(email)
72
- strong_node = document.css('strong').first
73
- stylings = SpecHelpers.styling_of_node(strong_node)
74
- stylings.should include(['box-shadow', '#62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0'])
75
- stylings.should include(['-o-box-shadow', '#62b0d7 1px 1px 1px 1px inset, #aaaaaa 1px 1px 3px 0'])
8
+ it "adds missing structure" do
9
+ html = "<h1>Hello world!</h1>".encode("Shift_JIS")
10
+ document = Roadie::Document.new(html)
11
+ result = document.transform
12
+
13
+ unless defined?(JRuby)
14
+ # JRuby has a bug that makes DTD manipulation impossible
15
+ # See Nokogiri bugs #984 and #985
16
+ # https://github.com/sparklemotion/nokogiri/issues/984
17
+ # https://github.com/sparklemotion/nokogiri/issues/985
18
+ result.should include("<!DOCTYPE html>")
76
19
  end
77
20
 
78
- it "only removes the css option when disabled" do
79
- Rails.application.config.roadie.enabled = false
80
-
81
- email = mailer.notification('doe@example.com', 'your quota limit has been reached')
21
+ result.should include("<html>")
22
+ result.should include("<head>")
23
+ result.should include("<body>")
82
24
 
83
- email.header.fields.map(&:name).should_not include('css')
25
+ result.should include("<meta")
26
+ result.should include("text/html; charset=Shift_JIS")
27
+ end
84
28
 
85
- email.to.should == ['doe@example.com']
86
- email.from.should == ['john@example.com']
87
- email.should have(2).parts
29
+ it "inlines given css" do
30
+ document = Roadie::Document.new <<-HTML
31
+ <html>
32
+ <head>
33
+ <title>Hello world!</title>
34
+ </head>
35
+ <body>
36
+ <h1>Hello world!</h1>
37
+ <p>Check out these <em>awesome</em> prices!</p>
38
+ </body>
39
+ </html>
40
+ HTML
41
+ document.add_css <<-CSS
42
+ em { color: red; }
43
+ h1 { text-align: center; }
44
+ CSS
45
+
46
+ result = parse_html document.transform
47
+ result.should have_styling('text-align' => 'center').at_selector('h1')
48
+ result.should have_styling('color' => 'red').at_selector('p > em')
49
+ end
88
50
 
89
- html = email.html_part.body.decoded
90
- html.should_not include '<!DOCTYPE'
91
- html.should_not include '<head'
51
+ it "inlines css from disk" do
52
+ document = Roadie::Document.new <<-HTML
53
+ <!DOCTYPE html>
54
+ <html>
55
+ <head>
56
+ <title>Hello world!</title>
57
+ <link rel="stylesheet" href="/spec/fixtures/big_em.css">
58
+ </head>
59
+ <body>
60
+ <h1>Hello world!</h1>
61
+ <p>Check out these <em>awesome</em> prices!</p>
62
+ </body>
63
+ </html>
64
+ HTML
65
+
66
+ result = parse_html document.transform
67
+ result.should have_styling('font-size' => '200%').at_selector('p > em')
68
+ end
92
69
 
93
- document = parse_html_in_email(email)
94
- document.should_not have_styling('color' => '#eee').at_selector('h1')
95
- document.should_not have_styling('background' => 'url(https://example.app.org/images/dots.png) repeat-x').at_selector('body')
96
- end
70
+ it "crashes when stylesheets cannot be found, unless using NullProvider" do
71
+ document = Roadie::Document.new <<-HTML
72
+ <!DOCTYPE html>
73
+ <html>
74
+ <head>
75
+ <link rel="stylesheet" href="/spec/fixtures/does_not_exist.css">
76
+ </head>
77
+ <body>
78
+ </body>
79
+ </html>
80
+ HTML
81
+
82
+ expect { document.transform }.to raise_error(Roadie::CssNotFound, /does_not_exist\.css/)
83
+
84
+ document.asset_providers << Roadie::NullProvider.new
85
+ expect { document.transform }.to_not raise_error
97
86
  end
98
87
 
99
- describe "filesystem integration" do
100
- it_behaves_like "roadie integration" do
101
- before(:each) { Rails.application.config.assets.enabled = false }
102
- end
88
+ it "makes URLs absolute" do
89
+ document = Roadie::Document.new <<-HTML
90
+ <!DOCTYPE html>
91
+ <html>
92
+ <head>
93
+ <style>
94
+ body { background: url("/assets/bg-abcdef1234567890.png"); }
95
+ </style>
96
+ <link rel="stylesheet" href="/style.css">
97
+ </head>
98
+ <body>
99
+ <a href="/about_us"><img src="/assets/about_us-abcdef1234567890.png" alt="About us"></a>
100
+ </body>
101
+ </html>
102
+ HTML
103
+
104
+ document.asset_providers = TestProvider.new(
105
+ "/style.css" => "a { background: url(/assets/link-abcdef1234567890.png); }"
106
+ )
107
+ document.url_options = {host: "myapp.com", scheme: "https", path: "rails/app/"}
108
+ result = parse_html document.transform
109
+
110
+ result.at_css("a")["href"].should == "https://myapp.com/rails/app/about_us"
111
+
112
+ result.at_css("img")["src"].should == "https://myapp.com/rails/app/assets/about_us-abcdef1234567890.png"
113
+
114
+ result.should have_styling(
115
+ "background" => 'url("https://myapp.com/rails/app/assets/bg-abcdef1234567890.png")'
116
+ ).at_selector("body")
117
+
118
+ result.should have_styling(
119
+ "background" => 'url(https://myapp.com/rails/app/assets/link-abcdef1234567890.png)'
120
+ ).at_selector("a")
103
121
  end
104
122
 
105
- describe "asset pipeline integration" do
106
- it_behaves_like "roadie integration" do
107
- before(:each) { Rails.application.config.assets.enabled = true }
108
- end
123
+ it "allows custom callbacks during inlining" do
124
+ document = Roadie::Document.new <<-HTML
125
+ <!DOCTYPE html>
126
+ <html>
127
+ <body>
128
+ <span>Hello world</span>
129
+ </body>
130
+ </html>
131
+ HTML
132
+
133
+ document.before_transformation = proc { |dom| dom.at_css("body")["class"] = "roadie" }
134
+ document.after_transformation = proc { |dom| dom.at_css("span").remove }
135
+
136
+ result = parse_html document.transform
137
+ result.at_css("body")["class"].should == "roadie"
138
+ result.at_css("span").should be_nil
109
139
  end
110
140
  end