spec/lib/roadie/inliner_spec.rb in roadie-2.4.3 vs spec/lib/roadie/inliner_spec.rb in roadie-3.0.0.pre1

- old
+ new

@@ -1,582 +1,150 @@ # encoding: UTF-8 require 'spec_helper' -describe Roadie::Inliner do - let(:provider) { double("asset provider", :all => '') } +module Roadie + describe Inliner do + before { @stylesheet = "" } + def use_css(css) @stylesheet = Stylesheet.new("example", css) end - def use_css(css) - provider.stub(:all).with(['global.css']).and_return(css) - end - - def rendering(html, options = {}) - url_options = options.fetch(:url_options, {:host => 'example.com'}) - after_inlining_handler = options[:after_inlining_handler] - Nokogiri::HTML.parse Roadie::Inliner.new(provider, ['global.css'], html, url_options, after_inlining_handler).execute - end - - describe "initialization" do - it "warns about asset_path_prefix being non-functional" do - expect { - Roadie::Inliner.new(provider, [], '', :asset_path_prefix => 'foo') - }.to raise_error(ArgumentError, /asset_path_prefix/) + def rendering(html, stylesheet = @stylesheet) + dom = Nokogiri::HTML.parse html + Inliner.new([stylesheet]).inline(dom) + dom end - end - describe "inlining styles" do - before(:each) do - # Make sure to have some css even when we don't specify any - # We have specific tests for when this is nil - use_css '' - end - - it "inlines simple attributes" do - use_css 'p { color: green }' - rendering('<p></p>').should have_styling('color' => 'green') - end - - it "inlines browser-prefixed attributes" do - use_css 'p { -vendor-color: green }' - rendering('<p></p>').should have_styling('-vendor-color' => 'green') - end - - it "inlines CSS3 attributes" do - use_css 'p { border-radius: 2px; }' - rendering('<p></p>').should have_styling('border-radius' => '2px') - end - - it "keeps the order of the styles that are inlined" do - use_css 'h1 { padding: 2px; margin: 5px; }' - rendering('<h1></h1>').should have_styling([['padding', '2px'], ['margin', '5px']]) - end - - it "combines multiple selectors into one" do - use_css 'p { color: green; } - .tip { float: right; }' - rendering('<p class="tip"></p>').should have_styling([['color', 'green'], ['float', 'right']]) - end - - it "uses the attributes with the highest specificity when conflicts arises" do - use_css "p { color: red; } - .safe { color: green; }" - rendering('<p class="safe"></p>').should have_styling('color' => 'green') - end - - it "sorts styles by specificity order" do - use_css 'p { margin: 2px; } - #big { margin: 10px; } - .down { margin-bottom: 5px; }' - - rendering('<p class="down"></p>').should have_styling([ - ['margin', '2px'], ['margin-bottom', '5px'] - ]) - - rendering('<p class="down" id="big"></p>').should have_styling([ - ['margin-bottom', '5px'], ['margin', '10px'] - ]) - end - - it "supports multiple selectors for the same rules" do - use_css 'p, a { color: green; }' - rendering('<p></p><a></a>').tap do |document| - document.should have_styling('color' => 'green').at_selector('p') - document.should have_styling('color' => 'green').at_selector('a') + describe "inlining styles" do + it "inlines simple attributes" do + use_css 'p { color: green }' + rendering('<p></p>').should have_styling('color' => 'green') end - end - it "keeps !important properties" do - use_css "a { text-decoration: underline !important; } - a.hard-to-spot { text-decoration: none; }" - rendering('<a class="hard-to-spot"></a>').should have_styling('text-decoration' => 'underline !important') - end - - it "combines with already present inline styles" do - use_css "p { color: green }" - rendering('<p style="font-size: 1.1em"></p>').should have_styling([['color', 'green'], ['font-size', '1.1em']]) - end - - it "does not touch already present inline styles" do - use_css "p { color: red }" - rendering('<p style="color: green"></p>').should have_styling([['color', 'red'], ['color', 'green']]) - end - - it "does not apply link and dynamic pseudo selectors" do - use_css " - p:active { color: red } - p:focus { color: red } - p:hover { color: red } - p:link { color: red } - p:target { color: red } - p:visited { color: red } - - p.active { width: 100%; } - " - rendering('<p class="active"></p>').should have_styling('width' => '100%') - end - - it "does not crash on any pseudo element selectors" do - use_css " - p.some-element { width: 100%; } - p::some-element { color: red; } - " - rendering('<p class="some-element"></p>').should have_styling('width' => '100%') - end - - it "works with nth-child" do - use_css " - p { color: red; } - p:nth-child(2n) { color: green; } - " - rendering(" - <p class='one'></p> - <p class='two'></p> - ").should have_styling('color' => 'green').at_selector('.two') - end - - it "ignores selectors with @" do - use_css '@keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } - }' - expect { rendering('<p></p>') }.not_to raise_error - end - - it 'ignores HTML comments and CDATA sections' do - # TinyMCE posts invalid CSS. We support that just to be pragmatic. - use_css %(<![CDATA[ - <!-- - p { color: green } - --> - ]]>) - expect { rendering '<p></p>' }.not_to raise_error - - use_css %( - <!-- - <![CDATA[ - <![CDATA[ - span { color: red } - ]]> - --> - ) - expect { rendering '<p></p>' }.not_to raise_error - end - - it "does not pick up scripts generating styles" do - expect { - rendering <<-HTML - <script> - var color = "red"; - document.write("<style type='text/css'>p { color: " + color + "; }</style>"); - </script> - HTML - }.not_to raise_error - end - - describe "inline <style> element" do - it "is used for inlined styles" do - rendering(<<-HTML).should have_styling([['color', 'green'], ['font-size', '1.1em']]) - <html> - <head> - <style type="text/css">p { color: green; }</style> - </head> - <body> - <p>Hello World</p> - <style type="text/css">p { font-size: 1.1em; }</style> - </body> - </html> - HTML + it "inlines browser-prefixed attributes" do + use_css 'p { -vendor-color: green }' + rendering('<p></p>').should have_styling('-vendor-color' => 'green') end - it "is removed" do - rendering(<<-HTML).should_not have_selector('style') - <html> - <head> - <style type="text/css">p { color: green; }</style> - </head> - <body> - <style type="text/css">p { font-size: 1.1em; }</style> - </body> - </html> - HTML + it "inlines CSS3 attributes" do + use_css 'p { border-radius: 2px; }' + rendering('<p></p>').should have_styling('border-radius' => '2px') end - it "is not touched when data-immutable is set" do - document = rendering <<-HTML - <style type="text/css" data-immutable>p { color: red; }</style> - <p></p> - HTML - document.should have_selector('style[data-immutable]') - document.should_not have_styling('color' => 'red') + it "keeps the order of the styles that are inlined" do + use_css 'h1 { padding: 2px; margin: 5px; }' + rendering('<h1></h1>').should have_styling([['padding', '2px'], ['margin', '5px']]) end - it "is not touched when for print media" do - document = rendering <<-HTML - <style type="text/css" media="print">p { color: red; }</style> - <p></p> - HTML - document.should have_selector('style[media=print]') - document.should_not have_styling('color' => 'red').at_selector('p') + it "combines multiple selectors into one" do + use_css 'p { color: green; } + .tip { float: right; }' + rendering('<p class="tip"></p>').should have_styling([['color', 'green'], ['float', 'right']]) end - it "is still inlined when no external css rules are defined" do - # This is just testing that the main code paths are still active even - # when css is set to nil - use_css nil - rendering(<<-HTML).should have_styling('color' => 'green').at_selector('p') - <style type="text/css">p { color: green; }</style> - <p>Hello World</p> - HTML + it "uses the attributes with the highest specificity when conflicts arises" do + use_css ".safe { color: green; } + p { color: red; }" + rendering('<p class="safe"></p>').should have_styling([['color', 'red'], ['color', 'green']]) end - it "is not touched when inside a SVG element" do - expect { - rendering <<-HTML - <p>Hello World</p> - <svg> - <style>This is not parseable by the CSS parser!</style> - </svg> - HTML - }.to_not raise_error - end - end - end + it "sorts styles by specificity order" do + use_css 'p { important: no; } + #important { important: very; } + .important { important: yes; }' - describe "linked stylesheets" do - def fake_file(name, contents) - provider.should_receive(:find).with(name).and_return(contents) - end + rendering('<p class="important"></p>').should have_styling([ + %w[important no], %w[important yes] + ]) - it "inlines styles from the linked stylesheet" do - fake_file('/assets/green_paragraphs.css', 'p { color: green; }') - - rendering(<<-HTML).should have_styling('color' => 'green').at_selector('p') - <html> - <head> - <link rel="stylesheet" href="/assets/green_paragraphs.css"> - </head> - <body> - <p></p> - </body> - </html> - HTML - end - - it "inlines styles from the linked stylesheet in subdirectory" do - fake_file('/assets/subdirectory/red_paragraphs.css', 'p { color: red; }') - - rendering(<<-HTML).should have_styling('color' => 'red').at_selector('p') - <html> - <head> - <link rel="stylesheet" href="/assets/subdirectory/red_paragraphs.css"> - </head> - <body> - <p></p> - </body> - </html> - HTML - end - - it "inlines styles from more than one linked stylesheet" do - fake_file('/assets/large_purple_paragraphs.css', 'p { font-size: 18px; color: purple; }') - fake_file('/assets/green_paragraphs.css', 'p { color: green; }') - - html = <<-HTML - <html> - <head> - <link rel="stylesheet" href="/assets/large_purple_paragraphs.css"> - <link rel="stylesheet" href="/assets/green_paragraphs.css"> - </head> - <body> - <p></p> - </body> - </html> - HTML - - rendering(html).should have_styling([ - ['font-size', '18px'], - ['color', 'green'], - ]).at_selector('p') - end - - it "removes the stylesheet links from the DOM" do - provider.stub(:find => '') - rendering(<<-HTML).should_not have_selector('link') - <html> - <head> - <link rel="stylesheet" href="/assets/green_paragraphs.css"> - <link rel="stylesheet" href="/assets/large_purple_paragraphs.css"> - </head> - <body> - </body> - </html> - HTML - end - - context "when stylesheet is for print media" do - it "does not inline the stylesheet" do - rendering(<<-HTML).should_not have_styling('color' => 'green').at_selector('p') - <html> - <head> - <link rel="stylesheet" href="/assets/green_paragraphs.css" media="print"> - </head> - <body> - <p></p> - </body> - </html> - HTML + rendering('<p class="important" id="important"></p>').should have_styling([ + %w[important no], %w[important yes], %w[important very] + ]) end - it "does not remove the links" do - rendering(<<-HTML).should have_selector('link') - <html> - <head> - <link rel="stylesheet" href="/assets/green_paragraphs.css" media="print"> - </head> - <body> - </body> - </html> - HTML + it "supports multiple selectors for the same rules" do + use_css 'p, a { color: green; }' + rendering('<p></p><a></a>').tap do |document| + document.should have_styling('color' => 'green').at_selector('p') + document.should have_styling('color' => 'green').at_selector('a') + end end - end - context "when stylesheet is marked as immutable" do - it "does not inline the stylesheet" do - rendering(<<-HTML).should_not have_styling('color' => 'green').at_selector('p') - <html> - <head> - <link rel="stylesheet" href="/assets/green_paragraphs.css" data-immutable="true"> - </head> - <body> - <p></p> - </body> - </html> - HTML + it "keeps !important properties" do + use_css "a { text-decoration: underline !important; } + a.hard-to-spot { text-decoration: none; }" + rendering('<a class="hard-to-spot"></a>').should have_styling([ + ['text-decoration', 'none'], ['text-decoration', 'underline !important'] + ]) end - it "does not remove link" do - rendering(<<-HTML).should have_selector('link') - <html> - <head> - <link rel="stylesheet" href="/assets/green_paragraphs.css" data-immutable="true"> - </head> - <body> - </body> - </html> - HTML + it "combines with already present inline styles" do + use_css "p { color: green }" + rendering('<p style="font-size: 1.1em"></p>').should have_styling([['color', 'green'], ['font-size', '1.1em']]) end - end - context "when stylesheet link uses an absolute URL" do - it "does not inline the stylesheet" do - rendering(<<-HTML).should_not have_styling('color' => 'green').at_selector('p') - <html> - <head> - <link rel="stylesheet" href="http://www.example.com/green_paragraphs.css"> - </head> - <body> - <p></p> - </body> - </html> - HTML + it "does not override inline styles" do + use_css "p { text-transform: uppercase; color: red }" + # The two color properties are kept to make css fallbacks work correctly + rendering('<p style="color: green"></p>').should have_styling([ + ['text-transform', 'uppercase'], + ['color', 'red'], + ['color', 'green'], + ]) end - it "does not remove link" do - rendering(<<-HTML).should have_selector('link') - <html> - <head> - <link rel="stylesheet" href="http://www.example.com/green_paragraphs.css"> - </head> - <body> - </body> - </html> - HTML - end - end + it "does not apply link and dynamic pseudo selectors" do + use_css " + p:active { color: red } + p:focus { color: red } + p:hover { color: red } + p:link { color: red } + p:target { color: red } + p:visited { color: red } - context "stylesheet cannot be found on disk" do - it "raises an error" do - html = <<-HTML - <html> - <head> - <link rel="stylesheet" href="/assets/not_found.css"> - </head> - <body> - </body> - </html> - HTML - - expect { rendering(html) }.to raise_error do |error| - error.should be_a(Roadie::CSSFileNotFound) - error.filename.should == Roadie.app.assets['not_found.css'] - error.guess.should == '/assets/not_found.css' - end + p.active { width: 100%; } + " + rendering('<p class="active"></p>').should have_styling('width' => '100%') end - end - context "link element is not for a stylesheet" do - it "is ignored" do - html = <<-HTML - <html> - <head> - <link rel="not_stylesheet" href="/assets/green_paragraphs.css"> - </head> - <body> - <p></p> - </body> - </html> - HTML - rendering(html).tap do |document| - document.should_not have_styling('color' => 'green').at_selector('p') - document.should have_selector('link') - end + it "does not crash on any pseudo element selectors" do + use_css " + p.some-element { width: 100%; } + p::some-element { color: red; } + " + rendering('<p class="some-element"></p>').should have_styling('width' => '100%') end - end - end - describe "making urls absolute" do - it "works on image sources" do - rendering('<img src="/images/foo.jpg" />').should have_attribute('src' => 'http://example.com/images/foo.jpg') - rendering('<img src="../images/foo.jpg" />').should have_attribute('src' => 'http://example.com/images/foo.jpg') - rendering('<img src="foo.jpg" />').should have_attribute('src' => 'http://example.com/foo.jpg') - end + it "warns on selectors that crash Nokogiri" do + dom = Nokogiri::HTML.parse "<p></p>" - it "does not touch image sources that are already absolute" do - rendering('<img src="http://other.example.org/images/foo.jpg" />').should have_attribute('src' => 'http://other.example.org/images/foo.jpg') - end - - it "works on inlined style attributes" do - rendering('<p style="background: url(/paper.png)"></p>').should have_styling('background' => 'url(http://example.com/paper.png)') - rendering('<p style="background: url(&quot;/paper.png&quot;)"></p>').should have_styling('background' => 'url("http://example.com/paper.png")') - end - - it "works on external style declarations" do - use_css "p { background-image: url(/paper.png); } - table { background-image: url('/paper.png'); } - div { background-image: url(\"/paper.png\"); }" - rendering('<p></p>').should have_styling('background-image' => 'url(http://example.com/paper.png)') - rendering('<table></table>').should have_styling('background-image' => "url('http://example.com/paper.png')") - rendering('<div></div>').should have_styling('background-image' => 'url("http://example.com/paper.png")') - end - - it "does not touch style urls that are already absolute" do - external_url = 'url(http://other.example.org/paper.png)' - use_css "p { background-image: #{external_url}; }" - rendering('<p></p>').should have_styling('background-image' => external_url) - rendering(%(<div style="background-image: #{external_url}"></div>)).should have_styling('background-image' => external_url) - end - - it "does not touch the urls when no url options are defined" do - use_css "img { background: url(/a.jpg); }" - rendering('<img src="/b.jpg" />', :url_options => nil).tap do |document| - document.should have_attribute('src' => '/b.jpg').at_selector('img') - document.should have_styling('background' => 'url(/a.jpg)') + stylesheet = Stylesheet.new "foo.css", "p[%^=foo] { color: red; }" + inliner = Inliner.new([stylesheet]) + inliner.should_receive(:warn).with( + %{Roadie cannot use "p[%^=foo]" (from "foo.css" stylesheet) when inlining stylesheets} + ) + inliner.inline(dom) end - end - it "supports port and protocol settings" do - use_css "img { background: url(/a.jpg); }" - rendering('<img src="/b.jpg" />', :url_options => {:host => 'example.com', :protocol => 'https', :port => '8080'}).tap do |document| - document.should have_attribute('src' => 'https://example.com:8080/b.jpg').at_selector('img') - document.should have_styling('background' => 'url(https://example.com:8080/a.jpg)') - end - end + it "works with nth-child" do + use_css " + p { color: red; } + p:nth-child(2n) { color: green; } + " + result = rendering("<p></p> <p></p>") - # This case was happening for some users when emails were rendered as part - # of the request cycle. I do not know it we *really* should accept these - # values, but it looks like Rails do accept it so we might as well do it - # too. - it "supports protocol settings with additional tokens" do - use_css "img { background: url(/a.jpg); }" - rendering('<img src="/b.jpg" />', :url_options => {:host => 'example.com', :protocol => 'https://'}).tap do |document| - document.should have_attribute('src' => 'https://example.com/b.jpg').at_selector('img') - document.should have_styling('background' => 'url(https://example.com/a.jpg)') + result.should have_styling([['color', 'red']]).at_selector('p:first') + result.should have_styling([['color', 'red'], ['color', 'green']]).at_selector('p:last') end - end - it "does not touch data: URIs" do - use_css "div { background: url(data:abcdef); }" - rendering('<div></div>').should have_styling('background' => 'url(data:abcdef)') - end - end - - describe "custom converter" do - let(:html) { '<div id="foo"></div>' } - - it "is invoked" do - after_inlining_handler = double("converter") - after_inlining_handler.should_receive(:call).with(anything) - rendering(html, :after_inlining_handler => after_inlining_handler) - end - - it "modifies the document using lambda" do - after_inlining_handler = lambda {|d| d.css("#foo").first["class"] = "bar"} - rendering(html, :after_inlining_handler => after_inlining_handler).css("#foo").first["class"].should == "bar" - end - - it "modifies the document using object" do - klass = Class.new do - def call(d) - d.css("#foo").first["class"] = "bar" - end - end - after_inlining_handler = klass.new - rendering(html, :after_inlining_handler => after_inlining_handler).css("#foo").first["class"].should == "bar" - end - end - - describe "inserting tags" do - it "inserts a doctype if not present" do - rendering('<html><body></body></html>').to_xml.should include('<!DOCTYPE ') - rendering('<!DOCTYPE html><html><body></body></html>').to_xml.should_not match(/(DOCTYPE.*?){2}/) - end - - it "sets xmlns of <html> to that of XHTML" do - rendering('<html><body></body></html>').should have_node('html').with_attributes("xmlns" => "http://www.w3.org/1999/xhtml") - end - - it "inserts basic html structure if not present" do - rendering('<h1>Hey!</h1>').should have_selector('html > head + body > h1') - end - - it "inserts <head> if not present" do - rendering('<html><body></body></html>').should have_selector('html > head + body') - end - - it "inserts meta tag describing content-type" do - rendering('<html><head></head><body></body></html>').tap do |document| - document.should have_selector('head meta[http-equiv="Content-Type"]') - document.css('head meta[http-equiv="Content-Type"]').first['content'].should == 'text/html; charset=UTF-8' - end - end - - it "does not insert duplicate meta tags describing content-type" do - rendering(<<-HTML).to_html.scan('meta').should have(1).item - <html> - <head> - <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" /> - </head> - </html> - HTML - end - end - - describe "css url regex" do - it "parses css urls" do - { - %{url(/foo.jpg)} => '/foo.jpg', - %{url("/foo.jpg")} => '/foo.jpg', - %{url('/foo.jpg')} => '/foo.jpg', - %{url(http://localhost/foo.jpg)} => 'http://localhost/foo.jpg', - %{url("http://localhost/foo.jpg")} => 'http://localhost/foo.jpg', - %{url('http://localhost/foo.jpg')} => 'http://localhost/foo.jpg', - %{url(/andromeda_(galaxy).jpg)} => '/andromeda_(galaxy).jpg', - }.each do |raw, expected| - raw =~ Roadie::Inliner::CSS_URL_REGEXP - $2.should == expected + it "ignores selectors with @" do + use_css '@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } + }' + expect { rendering('<p></p>') }.not_to raise_error end end end end