roadie 3.0.5 → 3.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -7
  3. data/.travis.yml +1 -5
  4. data/Changelog.md +20 -1
  5. data/Guardfile +2 -1
  6. data/README.md +193 -53
  7. data/lib/roadie.rb +3 -2
  8. data/lib/roadie/asset_scanner.rb +30 -10
  9. data/lib/roadie/cached_provider.rb +77 -0
  10. data/lib/roadie/document.rb +29 -10
  11. data/lib/roadie/errors.rb +43 -1
  12. data/lib/roadie/filesystem_provider.rb +3 -1
  13. data/lib/roadie/inliner.rb +57 -27
  14. data/lib/roadie/markup_improver.rb +2 -1
  15. data/lib/roadie/net_http_provider.rb +70 -0
  16. data/lib/roadie/null_provider.rb +3 -0
  17. data/lib/roadie/path_rewriter_provider.rb +64 -0
  18. data/lib/roadie/provider_list.rb +19 -1
  19. data/lib/roadie/rspec.rb +1 -0
  20. data/lib/roadie/rspec/cache_store.rb +25 -0
  21. data/lib/roadie/stylesheet.rb +1 -0
  22. data/lib/roadie/url_generator.rb +2 -1
  23. data/lib/roadie/utils.rb +9 -0
  24. data/lib/roadie/version.rb +1 -1
  25. data/roadie.gemspec +2 -1
  26. data/spec/hash_as_cache_store_spec.rb +7 -0
  27. data/spec/integration_spec.rb +91 -0
  28. data/spec/lib/roadie/asset_scanner_spec.rb +79 -25
  29. data/spec/lib/roadie/cached_provider_spec.rb +52 -0
  30. data/spec/lib/roadie/document_spec.rb +43 -7
  31. data/spec/lib/roadie/filesystem_provider_spec.rb +5 -0
  32. data/spec/lib/roadie/inliner_spec.rb +72 -15
  33. data/spec/lib/roadie/net_http_provider_spec.rb +89 -0
  34. data/spec/lib/roadie/path_rewriter_provider_spec.rb +39 -0
  35. data/spec/lib/roadie/provider_list_spec.rb +31 -8
  36. data/spec/lib/roadie/stylesheet_spec.rb +14 -8
  37. data/spec/lib/roadie/utils_spec.rb +7 -0
  38. data/spec/spec_helper.rb +1 -0
  39. data/spec/support/have_styling_matcher.rb +1 -0
  40. metadata +40 -9
  41. data/lib/roadie/upgrade_guide.rb +0 -36
@@ -0,0 +1,52 @@
1
+ require 'spec_helper'
2
+ require 'roadie/rspec'
3
+ require 'shared_examples/asset_provider'
4
+
5
+ module Roadie
6
+ describe CachedProvider do
7
+ let(:upstream) { TestProvider.new("good.css" => "body { color: green; }") }
8
+ let(:cache) { Hash.new }
9
+ subject(:provider) { CachedProvider.new(upstream, cache) }
10
+
11
+ it_behaves_like "roadie asset provider", valid_name: "good.css", invalid_name: "bad.css"
12
+
13
+ it "stores retrieved stylesheets in the cache" do
14
+ found = nil
15
+
16
+ expect {
17
+ found = provider.find_stylesheet("good.css")
18
+ }.to change(cache, :keys).to(["good.css"])
19
+
20
+ expect(cache["good.css"]).to eq found
21
+ end
22
+
23
+ it "reads from the cache first" do
24
+ found = upstream.find_stylesheet!("good.css")
25
+
26
+ cache["good.css"] = found
27
+
28
+ expect(upstream).to_not receive(:find_stylesheet)
29
+ expect(provider.find_stylesheet("good.css")).to eq found
30
+ expect(provider.find_stylesheet!("good.css")).to eq found
31
+ end
32
+
33
+ it "stores failed lookups in the cache" do
34
+ expect {
35
+ provider.find_stylesheet("foo.css")
36
+ }.to change(cache, :keys).to(["foo.css"])
37
+ expect(cache["foo.css"]).to be_nil
38
+ end
39
+
40
+ it "stores failed lookups even when raising errors" do
41
+ expect {
42
+ provider.find_stylesheet!("bar.css")
43
+ }.to raise_error CssNotFound
44
+ expect(cache.keys).to include "bar.css"
45
+ expect(cache["bar.css"]).to be_nil
46
+ end
47
+
48
+ it "defaults to a hash for cache storage" do
49
+ expect(CachedProvider.new(upstream).cache).to be_kind_of Hash
50
+ end
51
+ end
52
+ end
@@ -16,17 +16,26 @@ module Roadie
16
16
  expect(document.url_options).to eq({host: "foo.bar"})
17
17
  end
18
18
 
19
- it "has a ProviderList" do
19
+ it "has a setting for keeping uninlinable styles" do
20
+ expect(document.keep_uninlinable_css).to be true
21
+ document.keep_uninlinable_css = false
22
+ expect(document.keep_uninlinable_css).to be false
23
+ end
24
+
25
+ it "has a ProviderList for normal and external providers" do
20
26
  expect(document.asset_providers).to be_instance_of(ProviderList)
27
+ expect(document.external_asset_providers).to be_instance_of(ProviderList)
21
28
  end
22
29
 
23
- it "defaults to having just a FilesystemProvider in the provider list" do
30
+ it "defaults to having just a FilesystemProvider in the normal provider list" do
24
31
  expect(document).to have(1).asset_providers
32
+ expect(document).to have(0).external_asset_providers
33
+
25
34
  provider = document.asset_providers.first
26
35
  expect(provider).to be_instance_of(FilesystemProvider)
27
36
  end
28
37
 
29
- it "allows changes to the asset providers" do
38
+ it "allows changes to the normal asset providers" do
30
39
  other_provider = double "Other proider"
31
40
  old_list = document.asset_providers
32
41
 
@@ -38,6 +47,18 @@ module Roadie
38
47
  expect(document.asset_providers).to eq(old_list)
39
48
  end
40
49
 
50
+ it "allows changes to the external asset providers" do
51
+ other_provider = double "Other proider"
52
+ old_list = document.external_asset_providers
53
+
54
+ document.external_asset_providers = [other_provider]
55
+ expect(document.external_asset_providers).to be_instance_of(ProviderList)
56
+ expect(document.external_asset_providers.each.to_a).to eq([other_provider])
57
+
58
+ document.external_asset_providers = old_list
59
+ expect(document.external_asset_providers).to eq(old_list)
60
+ end
61
+
41
62
  it "can store callbacks for inlining" do
42
63
  callable = double "Callable"
43
64
 
@@ -51,17 +72,32 @@ module Roadie
51
72
  describe "transforming" do
52
73
  it "runs the before and after callbacks" do
53
74
  document = Document.new "<body></body>"
54
- before = double call: nil
55
- after = double call: nil
75
+ before = ->{}
76
+ after = ->{}
56
77
  document.before_transformation = before
57
78
  document.after_transformation = after
58
79
 
59
- expect(before).to receive(:call).with(instance_of(Nokogiri::HTML::Document)).ordered
80
+ expect(before).to receive(:call).with(instance_of(Nokogiri::HTML::Document), document).ordered
60
81
  expect(Inliner).to receive(:new).ordered.and_return double.as_null_object
61
- expect(after).to receive(:call).with(instance_of(Nokogiri::HTML::Document)).ordered
82
+ expect(after).to receive(:call).with(instance_of(Nokogiri::HTML::Document), document).ordered
62
83
 
63
84
  document.transform
64
85
  end
86
+
87
+ # TODO: Remove on next major version.
88
+ it "works on callables that don't expect more than one argument" do
89
+ document = Document.new "<body></body>"
90
+ document.before_transformation = ->(first) { }
91
+ document.after_transformation = ->(first = nil) { }
92
+
93
+ expect { document.transform }.to_not raise_error
94
+
95
+ # It still supplies the second argument, if possible.
96
+ document.after_transformation = ->(first, second = nil) {
97
+ raise "Oops" unless second
98
+ }
99
+ expect { document.transform }.to_not raise_error
100
+ end
65
101
  end
66
102
  end
67
103
 
@@ -18,6 +18,11 @@ module Roadie
18
18
  expect(FilesystemProvider.new.path).to eq(Dir.pwd)
19
19
  end
20
20
 
21
+ it "shows the given path in string representation" do
22
+ expect(provider.to_s).to include provider.path.to_s
23
+ expect(provider.inspect).to include provider.path.to_s
24
+ end
25
+
21
26
  describe "finding stylesheets" do
22
27
  it "finds files in the path" do
23
28
  full_path = File.join(fixtures_path, "stylesheets", "green.css")
@@ -8,7 +8,7 @@ module Roadie
8
8
 
9
9
  def rendering(html, stylesheet = @stylesheet)
10
10
  dom = Nokogiri::HTML.parse html
11
- Inliner.new([stylesheet]).inline(dom)
11
+ Inliner.new([stylesheet], dom).inline
12
12
  dom
13
13
  end
14
14
 
@@ -116,11 +116,11 @@ module Roadie
116
116
  dom = Nokogiri::HTML.parse "<p></p>"
117
117
 
118
118
  stylesheet = Stylesheet.new "foo.css", "p[%^=foo] { color: red; }"
119
- inliner = Inliner.new([stylesheet])
120
- expect(inliner).to receive(:warn).with(
121
- %{Roadie cannot use "p[%^=foo]" (from "foo.css" stylesheet) when inlining stylesheets}
119
+ inliner = Inliner.new([stylesheet], dom)
120
+ expect(Utils).to receive(:warn).with(
121
+ %{Cannot inline "p[%^=foo]" from "foo.css" stylesheet. If this is valid CSS, please report a bug.}
122
122
  )
123
- inliner.inline(dom)
123
+ inliner.inline
124
124
  end
125
125
 
126
126
  it "works with nth-child" do
@@ -134,16 +134,73 @@ module Roadie
134
134
  expect(result).to have_styling([['color', 'red'], ['color', 'green']]).at_selector('p:last')
135
135
  end
136
136
 
137
- it "ignores selectors with @" do
138
- use_css '@keyframes progress-bar-stripes {
139
- from {
140
- background-position: 40px 0;
141
- }
142
- to {
143
- background-position: 0 0;
144
- }
145
- }'
146
- expect { rendering('<p></p>') }.not_to raise_error
137
+ context "with uninlinable selectors" do
138
+ before do
139
+ allow(Roadie::Utils).to receive(:warn)
140
+ end
141
+
142
+ it "puts them in a new <style> element in the <head>" do
143
+ use_css 'a:hover { color: red; }'
144
+ result = rendering("
145
+ <html>
146
+ <head></head>
147
+ <body><a></a></body>
148
+ </html>
149
+ ")
150
+ expect(result).to have_selector("head > style")
151
+ expect(result.at_css("head > style").text).to eq "a:hover{color:red}"
152
+ end
153
+
154
+ it "puts them in <head> on unexpected inlining problems" do
155
+ use_css 'p:some-future-thing { color: red; }'
156
+ result = rendering("
157
+ <html>
158
+ <head></head>
159
+ <body><p></p></body>
160
+ </html>
161
+ ")
162
+ expect(result).to have_selector("head > style")
163
+ expect(result.at_css("head > style").text).to eq "p:some-future-thing{color:red}"
164
+ end
165
+
166
+ # This is not really wanted behavior, but there's nothing we can do
167
+ # about it because of limitations on CSS Parser.
168
+ it "puts does not put keyframes in <head>" do
169
+ css = '@keyframes progress-bar-stripes {
170
+ from {
171
+ background-position: 40px 0;
172
+ }
173
+ to {
174
+ background-position: 0 0;
175
+ }
176
+ }'
177
+
178
+ use_css css
179
+ result = rendering('<p></p>')
180
+
181
+ expect(result).to have_styling([]).at_selector("p")
182
+
183
+ # css_parser actually sees an empty @keyframes on JRuby, and nothing
184
+ # on the others
185
+ if (style_element = result.at_css("head > style"))
186
+ expect(style_element.text).to_not include "background-position"
187
+ end
188
+ end
189
+
190
+ it "ignores them if told not to keep them" do
191
+ stylesheet = use_css "
192
+ p:hover { color: red; }
193
+ p:some-future-thing { color: red; }
194
+ "
195
+ dom = Nokogiri::HTML.parse "
196
+ <html>
197
+ <head></head>
198
+ <body><p></p></body>
199
+ </html>
200
+ "
201
+ Inliner.new([stylesheet], dom).inline(false)
202
+ expect(dom).to_not have_selector("head > style")
203
+ end
147
204
  end
148
205
  end
149
206
  end
@@ -0,0 +1,89 @@
1
+ require 'spec_helper'
2
+ require 'roadie/rspec'
3
+ require 'shared_examples/asset_provider'
4
+
5
+ module Roadie
6
+ describe NetHttpProvider do
7
+ around do |example|
8
+ WebMock.disable_net_connect!
9
+ example.run
10
+ WebMock.allow_net_connect!
11
+ end
12
+
13
+ url = "http://example.com/style.css".freeze
14
+
15
+ it_behaves_like(
16
+ "roadie asset provider",
17
+ valid_name: "http://example.com/green.css",
18
+ invalid_name: "http://example.com/red.css"
19
+ ) do
20
+ before do
21
+ stub_request(:get, "http://example.com/green.css").and_return(body: "p { color: green; }")
22
+ stub_request(:get, "http://example.com/red.css").and_return(status: 404, body: "Not here!")
23
+ end
24
+ end
25
+
26
+ describe "error handling" do
27
+ it "handles timeouts" do
28
+ stub_request(:get, url).and_timeout
29
+ expect {
30
+ NetHttpProvider.new.find_stylesheet!(url)
31
+ }.to raise_error CssNotFound, /timeout/i
32
+
33
+ expect { NetHttpProvider.new.find_stylesheet(url) }.to_not raise_error
34
+ end
35
+
36
+ it "displays response code and beginning of message body" do
37
+ stub_request(:get, url).and_return(
38
+ status: 503,
39
+ body: "Whoah there! Didn't you see we have a service window at this
40
+ time? It's kind of disrespectful not to remember everything I tell
41
+ you all the time!"
42
+ )
43
+
44
+ expect {
45
+ NetHttpProvider.new.find_stylesheet!(url)
46
+ }.to raise_error CssNotFound, /503.*whoah/i
47
+ end
48
+ end
49
+
50
+ describe "whitelist" do
51
+ it "can have a whitelist of host names" do
52
+ provider = NetHttpProvider.new(whitelist: ["example.com", "foo.bar"])
53
+ expect(provider.whitelist).to eq Set["example.com", "foo.bar"]
54
+ end
55
+
56
+ it "defaults to empty whitelist" do
57
+ expect(NetHttpProvider.new.whitelist).to eq Set[]
58
+ expect(NetHttpProvider.new(whitelist: nil).whitelist).to eq Set[]
59
+ end
60
+
61
+ it "will not download from other hosts if set" do
62
+ provider = NetHttpProvider.new(whitelist: ["whitelisted.example.com"])
63
+
64
+ whitelisted_url = "http://whitelisted.example.com/style.css"
65
+ other_url = "http://www.example.com/style.css"
66
+
67
+ whitelisted_request = stub_request(:get, whitelisted_url).and_return(body: "x")
68
+ other_request = stub_request(:get, other_url).and_return(body: "x")
69
+
70
+ expect(provider.find_stylesheet(other_url)).to be_nil
71
+ expect {
72
+ provider.find_stylesheet!(other_url)
73
+ }.to raise_error CssNotFound, /whitelist/
74
+
75
+ expect {
76
+ expect(provider.find_stylesheet(whitelisted_url)).to_not be_nil
77
+ provider.find_stylesheet!(whitelisted_url)
78
+ }.to_not raise_error
79
+
80
+ expect(whitelisted_request).to have_been_made.twice
81
+ expect(other_request).to_not have_been_made
82
+ end
83
+
84
+ it "is displayed in the string representation" do
85
+ expect(NetHttpProvider.new(whitelist: ["bar.baz"]).to_s).to include "bar.baz"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+ require 'roadie/rspec'
3
+ require 'shared_examples/asset_provider'
4
+
5
+ module Roadie
6
+ describe PathRewriterProvider do
7
+ let(:upstream) { TestProvider.new "good.css" => "body { color: green; }" }
8
+
9
+ subject(:provider) do
10
+ PathRewriterProvider.new(upstream) do |path|
11
+ path.gsub('well', 'good')
12
+ end
13
+ end
14
+
15
+ it_behaves_like "roadie asset provider", valid_name: "well.css", invalid_name: "bad"
16
+
17
+ it "does not call the upstream provider if block returns nil" do
18
+ provider = PathRewriterProvider.new(upstream) { nil }
19
+ expect(upstream).to_not receive(:find_stylesheet)
20
+ expect(upstream).to_not receive(:find_stylesheet!)
21
+
22
+ expect(provider.find_stylesheet("foo")).to be_nil
23
+ expect {
24
+ provider.find_stylesheet!("foo")
25
+ }.to raise_error(CssNotFound, /nil/)
26
+ end
27
+
28
+ it "does not call the upstream provider if block returns false" do
29
+ provider = PathRewriterProvider.new(upstream) { false }
30
+ expect(upstream).to_not receive(:find_stylesheet)
31
+ expect(upstream).to_not receive(:find_stylesheet!)
32
+
33
+ expect(provider.find_stylesheet("foo")).to be_nil
34
+ expect {
35
+ provider.find_stylesheet!("foo")
36
+ }.to raise_error(CssNotFound, /false/)
37
+ end
38
+ end
39
+ end
@@ -68,14 +68,37 @@ module Roadie
68
68
  provider = double("Provider", to_s: "Some provider")
69
69
  sublist = ProviderList.new([provider, provider])
70
70
  list = ProviderList.new([provider, sublist, provider])
71
- expect(list.to_s).to eql "ProviderList: [\n" +
72
- "\tSome provider,\n" +
73
- "\tProviderList: [\n" +
74
- "\t\tSome provider,\n" +
75
- "\t\tSome provider\n" +
76
- "\t],\n" +
77
- "\tSome provider\n" +
78
- "]"
71
+ expect(list.to_s).to eql(
72
+ "ProviderList: [\n" +
73
+ "\tSome provider,\n" +
74
+ "\tProviderList: [\n" +
75
+ "\t\tSome provider,\n" +
76
+ "\t\tSome provider\n" +
77
+ "\t],\n" +
78
+ "\tSome provider\n" +
79
+ "]"
80
+ )
81
+ end
82
+
83
+ it "raises a readable error message" do
84
+ provider = double("Provider", to_s: "Some provider")
85
+ allow(provider).to receive(:find_stylesheet!).and_raise(
86
+ CssNotFound.new("style.css", "I tripped", provider)
87
+ )
88
+
89
+ sublist = ProviderList.new([provider, provider])
90
+ list = ProviderList.new([provider, sublist, provider])
91
+
92
+ expect { list.find_stylesheet!("style.css") }.to raise_error { |error|
93
+ expect(error.message).to eq(
94
+ "Could not find stylesheet \"style.css\": All providers failed\n" +
95
+ "Used providers:\n" +
96
+ "\tSome provider: I tripped\n" +
97
+ "\tSome provider: I tripped\n" +
98
+ "\tSome provider: I tripped\n" +
99
+ "\tSome provider: I tripped\n"
100
+ )
101
+ }
79
102
  end
80
103
 
81
104
  describe "wrapping" do
@@ -21,16 +21,22 @@ module Roadie
21
21
  ])
22
22
  end
23
23
 
24
- it "can iterate all inlinable blocks" do
25
- inlinable = double(inlinable?: true, selector: "good", properties: "props")
26
- bad = double(inlinable?: false, selector: "bad", properties: "props")
24
+ if VERSION < "4.0"
25
+ it "can iterate all inlinable blocks" do
26
+ inlinable = double(inlinable?: true, selector: "good", properties: "props")
27
+ bad = double(inlinable?: false, selector: "bad", properties: "props")
27
28
 
28
- stylesheet = Stylesheet.new("example.css", "")
29
- allow(stylesheet).to receive_messages blocks: [bad, inlinable, bad]
29
+ stylesheet = Stylesheet.new("example.css", "")
30
+ allow(stylesheet).to receive_messages blocks: [bad, inlinable, bad]
30
31
 
31
- expect(stylesheet.each_inlinable_block.to_a).to eq([
32
- ["good", "props"],
33
- ])
32
+ expect(stylesheet.each_inlinable_block.to_a).to eq([
33
+ ["good", "props"],
34
+ ])
35
+ end
36
+ else
37
+ it "should no longer have #each_inlinable_block" do
38
+ fail "Remove #each_inlinable_block"
39
+ end
34
40
  end
35
41
 
36
42
  it "has a string representation of the contents" do