├── .gitignore
├── .travis.yml
├── Appraisals
├── Gemfile
├── Guardfile
├── LICENSE
├── README.md
├── Rakefile
├── config.ru
├── features
├── skip_certain_browsers.feature
├── step_definitions
│ ├── given
│ │ └── i_have_a_rack_app_with_live_reload.rb
│ ├── then
│ │ └── i_should_not_have_livereload_code.rb
│ └── when
│ │ └── i_make_a_request_with_headers.rb
└── support
│ └── env.rb
├── gemfiles
├── rails32.gemfile
└── rails40.gemfile
├── index.html
├── js
├── WebSocketMain.swf
├── livereload.js
├── swfobject.js
└── web_socket.js
├── lib
├── rack-livereload.rb
└── rack
│ ├── livereload.rb
│ └── livereload
│ ├── body_processor.rb
│ └── processing_skip_analyzer.rb
├── rack-livereload.gemspec
├── skel
└── livereload.html.erb
└── spec
├── rack
├── livereload
│ ├── body_processor_spec.rb
│ └── processing_skip_analyzer_spec.rb
└── livereload_spec.rb
└── spec_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | .bundle
3 | Gemfile.lock
4 | gemfiles/*.lock
5 | pkg/*
6 | *.orig
7 | tmp/
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | rvm:
2 | - 1.9.3
3 | - 2.0.0
4 | branches:
5 | only:
6 | - master
7 | gemfile:
8 | - gemfiles/rails32.gemfile
9 | - gemfiles/rails40.gemfile
10 |
11 |
--------------------------------------------------------------------------------
/Appraisals:
--------------------------------------------------------------------------------
1 | appraise 'rails32' do
2 | gem 'rails', '~> 3.2.0'
3 | end
4 |
5 | appraise 'rails40' do
6 | gem 'rails', '~> 4.0.0'
7 | end
8 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org"
2 |
3 | # Specify your gem's dependencies in rack-livereload.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/Guardfile:
--------------------------------------------------------------------------------
1 | # A sample Guardfile
2 | # More info at https://github.com/guard/guard#readme
3 |
4 | guard 'rspec', :cli => '-c' do
5 | watch(%r{^spec/.+_spec\.rb$})
6 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
7 | watch('spec/spec_helper.rb') { "spec" }
8 | end
9 |
10 | guard 'livereload' do
11 | watch('index.html')
12 | end
13 |
14 | guard 'cucumber' do
15 | watch(%r{^features/.+\.feature$})
16 | watch(%r{^features/support/.+$}) { 'features' }
17 | end
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright © 2012 John Bintz
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the “Software”), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Rack::LiveReload
2 |
3 | _This fork is deprecated: Go check out https://github.com/onesupercoder/rack-livereload instead._
4 |
5 |
6 | [](https://codeclimate.com/github/johnbintz/rack-livereload)
7 |
8 | Hey, you've got [LiveReload](http://livereload.com/) in my [Rack](http://rack.rubyforge.org/)!
9 | No need for browser extensions anymore! Just plug it in your middleware stack and go!
10 | Even supports browsers without WebSockets!
11 |
12 | Use this with [guard-livereload](http://github.com/guard/guard-livereload) for maximum fun!
13 |
14 | ## Installation
15 |
16 | ### Rails
17 |
18 | Add the gem to your Gemfile.
19 |
20 | ```ruby
21 | gem "rack-livereload", group: :development
22 | ```
23 |
24 | Then add the middleware to your Rails middleware stack by editing your `config/environments/development.rb`.
25 |
26 | ```ruby
27 | # config/environments/development.rb
28 |
29 | MyApp::Application.configure do
30 | # Add Rack::LiveReload to the bottom of the middleware stack with the default options:
31 | config.middleware.insert_after ActionDispatch::Static, Rack::LiveReload
32 |
33 | # or, if you're using better_errors:
34 | config.middleware.insert_before Rack::Lock, Rack::LiveReload
35 |
36 | # ...
37 | end
38 | ```
39 |
40 | #### Tweaking the options
41 |
42 | ```ruby
43 | # Specifying Rack::LiveReload options.
44 | config.middleware.use(Rack::LiveReload,
45 | min_delay : 500, # default 1000
46 | max_delay : 10_000, # default 60_000
47 | live_reload_port : 56789, # default 35729
48 | host : 'myhost.cool.wow',
49 | ignore : [ %r{dont/modify\.html$} ]
50 | )
51 | ```
52 |
53 | In addition, Rack::LiveReload's position within middleware stack can be
54 | specified by inserting it relative to an exsiting middleware via
55 | `insert_before` or `insert_after`. See the [Rails on Rack: Adding a
56 | Middleware](http://guides.rubyonrails.org/rails_on_rack.html#adding-a-middleware)
57 | section for more detail.
58 |
59 | ### Sinatra / config.ru
60 |
61 | ``` ruby
62 | require 'rack-livereload'
63 |
64 | use Rack::LiveReload
65 | # ...or...
66 | use Rack::LiveReload, min_delay: 500, ...
67 | ```
68 |
69 | ## How it works
70 |
71 | The necessary `script` tag to bring in a copy of [livereload.js](https://github.com/livereload/livereload-js) is
72 | injected right after the opening `head` tag in any `text/html` pages that come through. The `script` tag is built in
73 | such a way that the `HTTP_HOST` is used as the LiveReload host, so you can connect from external machines (say, to
74 | `mycomputer:3000` instead of `localhost:3000`) and as long as the LiveReload port is accessible from the external machine,
75 | you'll connect and be LiveReloading away!
76 |
77 | ### Which LiveReload script does it use?
78 |
79 | * If you've got a LiveReload watcher running on the same machine as the app that responds
80 | to `http://localhost:35729/livereload.js`, that gets used, with the hostname being changed when
81 | injected into the HTML page.
82 | * If you don't, the copy vendored with rack-livereload is used.
83 | * You can force the use of either one (and save on the cost of checking to see if that file
84 | is available) with the middleware option `:source => :vendored` or `:source => :livereload`.
85 |
86 | ### How about non-WebSocket-enabled browsers?
87 |
88 | For browsers that don't support WebSockets, but do support Flash, [web-socket-js](https://github.com/gimite/web-socket-js)
89 | is loaded. By default, this is done transparently, so you'll get a copy of swfobject.js and web_socket.js loaded even if
90 | your browser doesn't need it. The SWF WebSocket implementor won't be loaded unless your browser has no native
91 | WebSockets support or if you force it in the middleware stack:
92 |
93 | ``` ruby
94 | use Rack::LiveReload, force_swf: true
95 | ```
96 |
97 | If you don't want any of the web-sockets-js code included at all, use the `no_swf` option:
98 |
99 | ``` ruby
100 | use Rack::LiveReload, no_swf: true
101 | ```
102 |
103 | Once more browsers support WebSockets than don't, this option will be reversed and you'll have
104 | to explicitly include the Flash shim.
105 |
106 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require 'bundler/setup'
3 | require 'appraisal'
4 |
5 | desc 'Update livereload.js'
6 | task :update_livereload_js do
7 | require 'httparty'
8 |
9 | File.open('js/livereload.js', 'wb') { |fh|
10 | fh.print HTTParty.get('https://raw.github.com/livereload/livereload-js/master/dist/livereload.js').body
11 | }
12 | end
13 |
14 | desc 'Update web-socket-js'
15 | task :update_web_socket_js do
16 | require 'httparty'
17 |
18 | %w{swfobject.js web_socket.js WebSocketMain.swf}.each do |file|
19 | File.open("js/#{file}", 'wb') do |fh|
20 | fh.print HTTParty.get("https://raw.github.com/gimite/web-socket-js/master/#{file}").body
21 | end
22 | end
23 | end
24 |
25 | require 'rspec/core/rake_task'
26 |
27 | RSpec::Core::RakeTask.new(:spec)
28 |
29 | require 'cucumber/rake/task'
30 |
31 | Cucumber::Rake::Task.new(:cucumber)
32 |
33 | task :default => [ :spec, :cucumber ]
34 |
35 |
--------------------------------------------------------------------------------
/config.ru:
--------------------------------------------------------------------------------
1 | require 'sinatra'
2 | $: << 'lib'
3 |
4 | require 'rack/livereload'
5 |
6 | use Rack::Logger
7 | use Rack::LiveReload
8 | run Rack::Directory.new('.')
9 |
10 | if false
11 |
12 | get '/' do
13 | File.read('index.html')
14 | end
15 |
16 | run Sinatra::Application
17 | end
18 |
--------------------------------------------------------------------------------
/features/skip_certain_browsers.feature:
--------------------------------------------------------------------------------
1 | Feature: Skip Certain Browsers
2 | Scenario Outline:
3 | Given I have a Rack app with Rack::LiveReload
4 | When I make a request to "/" with the following headers:
5 | | HTTP_USER_AGENT | |
6 | Then I should not have any Rack::LiveReload code
7 |
8 | Scenarios: Browsers to check for
9 | | user agent |
10 | | MSIE |
11 |
12 |
--------------------------------------------------------------------------------
/features/step_definitions/given/i_have_a_rack_app_with_live_reload.rb:
--------------------------------------------------------------------------------
1 | Given /^I have a Rack app with Rack::LiveReload$/ do
2 | @app = Rack::Builder.new do
3 | use Rack::LiveReload
4 |
5 | run lambda { |env| [ 200, { 'Content-Type' => 'text/html' }, [ "" ] ] }
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/features/step_definitions/then/i_should_not_have_livereload_code.rb:
--------------------------------------------------------------------------------
1 | Then /^I should not have any Rack::LiveReload code$/ do
2 | @response.body.should_not include("rack/livereload.js")
3 | end
4 |
5 |
--------------------------------------------------------------------------------
/features/step_definitions/when/i_make_a_request_with_headers.rb:
--------------------------------------------------------------------------------
1 | When /^I make a request to "([^"]*)" with the following headers:$/ do |uri, table|
2 | @request = Rack::MockRequest.new(@app)
3 |
4 | @response = @request.get(uri, table.rows_hash)
5 | end
6 |
7 |
--------------------------------------------------------------------------------
/features/support/env.rb:
--------------------------------------------------------------------------------
1 | require 'rack'
2 | require 'rack-livereload'
3 |
4 |
--------------------------------------------------------------------------------
/gemfiles/rails32.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "rails", "~> 3.2.0"
6 |
7 | gemspec :path=>"../"
--------------------------------------------------------------------------------
/gemfiles/rails40.gemfile:
--------------------------------------------------------------------------------
1 | # This file was generated by Appraisal
2 |
3 | source "http://rubygems.org"
4 |
5 | gem "rails", "~> 4.0.0"
6 |
7 | gemspec :path=>"../"
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 | HiRats
3 |
--------------------------------------------------------------------------------
/js/WebSocketMain.swf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnbintz/rack-livereload/8cf0ebec609ee34e781d50c4acb97530838748f4/js/WebSocketMain.swf
--------------------------------------------------------------------------------
/js/livereload.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var __customevents = {}, __protocol = {}, __connector = {}, __timer = {}, __options = {}, __reloader = {}, __livereload = {}, __less = {}, __startup = {};
3 |
4 | // customevents
5 | var CustomEvents;
6 | CustomEvents = {
7 | bind: function(element, eventName, handler) {
8 | if (element.addEventListener) {
9 | return element.addEventListener(eventName, handler, false);
10 | } else if (element.attachEvent) {
11 | element[eventName] = 1;
12 | return element.attachEvent('onpropertychange', function(event) {
13 | if (event.propertyName === eventName) {
14 | return handler();
15 | }
16 | });
17 | } else {
18 | throw new Error("Attempt to attach custom event " + eventName + " to something which isn't a DOMElement");
19 | }
20 | },
21 | fire: function(element, eventName) {
22 | var event;
23 | if (element.addEventListener) {
24 | event = document.createEvent('HTMLEvents');
25 | event.initEvent(eventName, true, true);
26 | return document.dispatchEvent(event);
27 | } else if (element.attachEvent) {
28 | if (element[eventName]) {
29 | return element[eventName]++;
30 | }
31 | } else {
32 | throw new Error("Attempt to fire custom event " + eventName + " on something which isn't a DOMElement");
33 | }
34 | }
35 | };
36 | __customevents.bind = CustomEvents.bind;
37 | __customevents.fire = CustomEvents.fire;
38 |
39 | // protocol
40 | var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError;
41 | var __indexOf = Array.prototype.indexOf || function(item) {
42 | for (var i = 0, l = this.length; i < l; i++) {
43 | if (this[i] === item) return i;
44 | }
45 | return -1;
46 | };
47 | __protocol.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6';
48 | __protocol.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7';
49 | __protocol.ProtocolError = ProtocolError = (function() {
50 | function ProtocolError(reason, data) {
51 | this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\".";
52 | }
53 | return ProtocolError;
54 | })();
55 | __protocol.Parser = Parser = (function() {
56 | function Parser(handlers) {
57 | this.handlers = handlers;
58 | this.reset();
59 | }
60 | Parser.prototype.reset = function() {
61 | return this.protocol = null;
62 | };
63 | Parser.prototype.process = function(data) {
64 | var command, message, options, _ref;
65 | try {
66 | if (!(this.protocol != null)) {
67 | if (data.match(/^!!ver:([\d.]+)$/)) {
68 | this.protocol = 6;
69 | } else if (message = this._parseMessage(data, ['hello'])) {
70 | if (!message.protocols.length) {
71 | throw new ProtocolError("no protocols specified in handshake message");
72 | } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) {
73 | this.protocol = 7;
74 | } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) {
75 | this.protocol = 6;
76 | } else {
77 | throw new ProtocolError("no supported protocols found");
78 | }
79 | }
80 | return this.handlers.connected(this.protocol);
81 | } else if (this.protocol === 6) {
82 | message = JSON.parse(data);
83 | if (!message.length) {
84 | throw new ProtocolError("protocol 6 messages must be arrays");
85 | }
86 | command = message[0], options = message[1];
87 | if (command !== 'refresh') {
88 | throw new ProtocolError("unknown protocol 6 command");
89 | }
90 | return this.handlers.message({
91 | command: 'reload',
92 | path: options.path,
93 | liveCSS: (_ref = options.apply_css_live) != null ? _ref : true
94 | });
95 | } else {
96 | message = this._parseMessage(data, ['reload', 'alert']);
97 | return this.handlers.message(message);
98 | }
99 | } catch (e) {
100 | if (e instanceof ProtocolError) {
101 | return this.handlers.error(e);
102 | } else {
103 | throw e;
104 | }
105 | }
106 | };
107 | Parser.prototype._parseMessage = function(data, validCommands) {
108 | var message, _ref;
109 | try {
110 | message = JSON.parse(data);
111 | } catch (e) {
112 | throw new ProtocolError('unparsable JSON', data);
113 | }
114 | if (!message.command) {
115 | throw new ProtocolError('missing "command" key', data);
116 | }
117 | if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) {
118 | throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data);
119 | }
120 | return message;
121 | };
122 | return Parser;
123 | })();
124 |
125 | // connector
126 | // Generated by CoffeeScript 1.3.3
127 | var Connector, PROTOCOL_6, PROTOCOL_7, Parser, Version, _ref;
128 |
129 | _ref = __protocol, Parser = _ref.Parser, PROTOCOL_6 = _ref.PROTOCOL_6, PROTOCOL_7 = _ref.PROTOCOL_7;
130 |
131 | Version = '2.0.8';
132 |
133 | __connector.Connector = Connector = (function() {
134 |
135 | function Connector(options, WebSocket, Timer, handlers) {
136 | var _this = this;
137 | this.options = options;
138 | this.WebSocket = WebSocket;
139 | this.Timer = Timer;
140 | this.handlers = handlers;
141 | this._uri = "ws://" + this.options.host + ":" + this.options.port + "/livereload";
142 | this._nextDelay = this.options.mindelay;
143 | this._connectionDesired = false;
144 | this.protocol = 0;
145 | this.protocolParser = new Parser({
146 | connected: function(protocol) {
147 | _this.protocol = protocol;
148 | _this._handshakeTimeout.stop();
149 | _this._nextDelay = _this.options.mindelay;
150 | _this._disconnectionReason = 'broken';
151 | return _this.handlers.connected(protocol);
152 | },
153 | error: function(e) {
154 | _this.handlers.error(e);
155 | return _this._closeOnError();
156 | },
157 | message: function(message) {
158 | return _this.handlers.message(message);
159 | }
160 | });
161 | this._handshakeTimeout = new Timer(function() {
162 | if (!_this._isSocketConnected()) {
163 | return;
164 | }
165 | _this._disconnectionReason = 'handshake-timeout';
166 | return _this.socket.close();
167 | });
168 | this._reconnectTimer = new Timer(function() {
169 | if (!_this._connectionDesired) {
170 | return;
171 | }
172 | return _this.connect();
173 | });
174 | this.connect();
175 | }
176 |
177 | Connector.prototype._isSocketConnected = function() {
178 | return this.socket && this.socket.readyState === this.WebSocket.OPEN;
179 | };
180 |
181 | Connector.prototype.connect = function() {
182 | var _this = this;
183 | this._connectionDesired = true;
184 | if (this._isSocketConnected()) {
185 | return;
186 | }
187 | this._reconnectTimer.stop();
188 | this._disconnectionReason = 'cannot-connect';
189 | this.protocolParser.reset();
190 | this.handlers.connecting();
191 | this.socket = new this.WebSocket(this._uri);
192 | this.socket.onopen = function(e) {
193 | return _this._onopen(e);
194 | };
195 | this.socket.onclose = function(e) {
196 | return _this._onclose(e);
197 | };
198 | this.socket.onmessage = function(e) {
199 | return _this._onmessage(e);
200 | };
201 | return this.socket.onerror = function(e) {
202 | return _this._onerror(e);
203 | };
204 | };
205 |
206 | Connector.prototype.disconnect = function() {
207 | this._connectionDesired = false;
208 | this._reconnectTimer.stop();
209 | if (!this._isSocketConnected()) {
210 | return;
211 | }
212 | this._disconnectionReason = 'manual';
213 | return this.socket.close();
214 | };
215 |
216 | Connector.prototype._scheduleReconnection = function() {
217 | if (!this._connectionDesired) {
218 | return;
219 | }
220 | if (!this._reconnectTimer.running) {
221 | this._reconnectTimer.start(this._nextDelay);
222 | return this._nextDelay = Math.min(this.options.maxdelay, this._nextDelay * 2);
223 | }
224 | };
225 |
226 | Connector.prototype.sendCommand = function(command) {
227 | if (this.protocol == null) {
228 | return;
229 | }
230 | return this._sendCommand(command);
231 | };
232 |
233 | Connector.prototype._sendCommand = function(command) {
234 | return this.socket.send(JSON.stringify(command));
235 | };
236 |
237 | Connector.prototype._closeOnError = function() {
238 | this._handshakeTimeout.stop();
239 | this._disconnectionReason = 'error';
240 | return this.socket.close();
241 | };
242 |
243 | Connector.prototype._onopen = function(e) {
244 | var hello;
245 | this.handlers.socketConnected();
246 | this._disconnectionReason = 'handshake-failed';
247 | hello = {
248 | command: 'hello',
249 | protocols: [PROTOCOL_6, PROTOCOL_7]
250 | };
251 | hello.ver = Version;
252 | if (this.options.ext) {
253 | hello.ext = this.options.ext;
254 | }
255 | if (this.options.extver) {
256 | hello.extver = this.options.extver;
257 | }
258 | if (this.options.snipver) {
259 | hello.snipver = this.options.snipver;
260 | }
261 | this._sendCommand(hello);
262 | return this._handshakeTimeout.start(this.options.handshake_timeout);
263 | };
264 |
265 | Connector.prototype._onclose = function(e) {
266 | this.protocol = 0;
267 | this.handlers.disconnected(this._disconnectionReason, this._nextDelay);
268 | return this._scheduleReconnection();
269 | };
270 |
271 | Connector.prototype._onerror = function(e) {};
272 |
273 | Connector.prototype._onmessage = function(e) {
274 | return this.protocolParser.process(e.data);
275 | };
276 |
277 | return Connector;
278 |
279 | })();
280 |
281 | // timer
282 | var Timer;
283 | var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
284 | __timer.Timer = Timer = (function() {
285 | function Timer(func) {
286 | this.func = func;
287 | this.running = false;
288 | this.id = null;
289 | this._handler = __bind(function() {
290 | this.running = false;
291 | this.id = null;
292 | return this.func();
293 | }, this);
294 | }
295 | Timer.prototype.start = function(timeout) {
296 | if (this.running) {
297 | clearTimeout(this.id);
298 | }
299 | this.id = setTimeout(this._handler, timeout);
300 | return this.running = true;
301 | };
302 | Timer.prototype.stop = function() {
303 | if (this.running) {
304 | clearTimeout(this.id);
305 | this.running = false;
306 | return this.id = null;
307 | }
308 | };
309 | return Timer;
310 | })();
311 | Timer.start = function(timeout, func) {
312 | return setTimeout(func, timeout);
313 | };
314 |
315 | // options
316 | var Options;
317 | __options.Options = Options = (function() {
318 | function Options() {
319 | this.host = null;
320 | this.port = RACK_LIVERELOAD_PORT;
321 | this.snipver = null;
322 | this.ext = null;
323 | this.extver = null;
324 | this.mindelay = 1000;
325 | this.maxdelay = 60000;
326 | this.handshake_timeout = 5000;
327 | }
328 | Options.prototype.set = function(name, value) {
329 | switch (typeof this[name]) {
330 | case 'undefined':
331 | break;
332 | case 'number':
333 | return this[name] = +value;
334 | default:
335 | return this[name] = value;
336 | }
337 | };
338 | return Options;
339 | })();
340 | Options.extract = function(document) {
341 | var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len2, _ref, _ref2;
342 | _ref = document.getElementsByTagName('script');
343 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
344 | element = _ref[_i];
345 | if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) {
346 | options = new Options();
347 | if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) {
348 | options.host = mm[1];
349 | if (mm[2]) {
350 | options.port = parseInt(mm[2], 10);
351 | }
352 | }
353 | if (m[2]) {
354 | _ref2 = m[2].split('&');
355 | for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
356 | pair = _ref2[_j];
357 | if ((keyAndValue = pair.split('=')).length > 1) {
358 | options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('='));
359 | }
360 | }
361 | }
362 | return options;
363 | }
364 | }
365 | return null;
366 | };
367 |
368 | // reloader
369 | // Generated by CoffeeScript 1.3.1
370 | (function() {
371 | var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl;
372 |
373 | splitUrl = function(url) {
374 | var hash, index, params;
375 | if ((index = url.indexOf('#')) >= 0) {
376 | hash = url.slice(index);
377 | url = url.slice(0, index);
378 | } else {
379 | hash = '';
380 | }
381 | if ((index = url.indexOf('?')) >= 0) {
382 | params = url.slice(index);
383 | url = url.slice(0, index);
384 | } else {
385 | params = '';
386 | }
387 | return {
388 | url: url,
389 | params: params,
390 | hash: hash
391 | };
392 | };
393 |
394 | pathFromUrl = function(url) {
395 | var path;
396 | url = splitUrl(url).url;
397 | if (url.indexOf('file://') === 0) {
398 | path = url.replace(/^file:\/\/(localhost)?/, '');
399 | } else {
400 | path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/');
401 | }
402 | return decodeURIComponent(path);
403 | };
404 |
405 | pickBestMatch = function(path, objects, pathFunc) {
406 | var bestMatch, object, score, _i, _len;
407 | bestMatch = {
408 | score: 0
409 | };
410 | for (_i = 0, _len = objects.length; _i < _len; _i++) {
411 | object = objects[_i];
412 | score = numberOfMatchingSegments(path, pathFunc(object));
413 | if (score > bestMatch.score) {
414 | bestMatch = {
415 | object: object,
416 | score: score
417 | };
418 | }
419 | }
420 | if (bestMatch.score > 0) {
421 | return bestMatch;
422 | } else {
423 | return null;
424 | }
425 | };
426 |
427 | numberOfMatchingSegments = function(path1, path2) {
428 | var comps1, comps2, eqCount, len;
429 | path1 = path1.replace(/^\/+/, '').toLowerCase();
430 | path2 = path2.replace(/^\/+/, '').toLowerCase();
431 | if (path1 === path2) {
432 | return 10000;
433 | }
434 | comps1 = path1.split('/').reverse();
435 | comps2 = path2.split('/').reverse();
436 | len = Math.min(comps1.length, comps2.length);
437 | eqCount = 0;
438 | while (eqCount < len && comps1[eqCount] === comps2[eqCount]) {
439 | ++eqCount;
440 | }
441 | return eqCount;
442 | };
443 |
444 | pathsMatch = function(path1, path2) {
445 | return numberOfMatchingSegments(path1, path2) > 0;
446 | };
447 |
448 | IMAGE_STYLES = [
449 | {
450 | selector: 'background',
451 | styleNames: ['backgroundImage']
452 | }, {
453 | selector: 'border',
454 | styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage']
455 | }
456 | ];
457 |
458 | __reloader.Reloader = Reloader = (function() {
459 |
460 | Reloader.name = 'Reloader';
461 |
462 | function Reloader(window, console, Timer) {
463 | this.window = window;
464 | this.console = console;
465 | this.Timer = Timer;
466 | this.document = this.window.document;
467 | this.importCacheWaitPeriod = 200;
468 | this.plugins = [];
469 | }
470 |
471 | Reloader.prototype.addPlugin = function(plugin) {
472 | return this.plugins.push(plugin);
473 | };
474 |
475 | Reloader.prototype.analyze = function(callback) {
476 | return results;
477 | };
478 |
479 | Reloader.prototype.reload = function(path, options) {
480 | var plugin, _base, _i, _len, _ref;
481 | this.options = options;
482 | if ((_base = this.options).stylesheetReloadTimeout == null) {
483 | _base.stylesheetReloadTimeout = 15000;
484 | }
485 | _ref = this.plugins;
486 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
487 | plugin = _ref[_i];
488 | if (plugin.reload && plugin.reload(path, options)) {
489 | return;
490 | }
491 | }
492 | if (options.liveCSS) {
493 | if (path.match(/\.css$/i)) {
494 | if (this.reloadStylesheet(path)) {
495 | return;
496 | }
497 | }
498 | }
499 | if (options.liveImg) {
500 | if (path.match(/\.(jpe?g|png|gif)$/i)) {
501 | this.reloadImages(path);
502 | return;
503 | }
504 | }
505 | return this.reloadPage();
506 | };
507 |
508 | Reloader.prototype.reloadPage = function() {
509 | return this.window.document.location.reload();
510 | };
511 |
512 | Reloader.prototype.reloadImages = function(path) {
513 | var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results;
514 | expando = this.generateUniqueString();
515 | _ref = this.document.images;
516 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
517 | img = _ref[_i];
518 | if (pathsMatch(path, pathFromUrl(img.src))) {
519 | img.src = this.generateCacheBustUrl(img.src, expando);
520 | }
521 | }
522 | if (this.document.querySelectorAll) {
523 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) {
524 | _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames;
525 | _ref2 = this.document.querySelectorAll("[style*=" + selector + "]");
526 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) {
527 | img = _ref2[_k];
528 | this.reloadStyleImages(img.style, styleNames, path, expando);
529 | }
530 | }
531 | }
532 | if (this.document.styleSheets) {
533 | _ref3 = this.document.styleSheets;
534 | _results = [];
535 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) {
536 | styleSheet = _ref3[_l];
537 | _results.push(this.reloadStylesheetImages(styleSheet, path, expando));
538 | }
539 | return _results;
540 | }
541 | };
542 |
543 | Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) {
544 | var rule, rules, styleNames, _i, _j, _len, _len1;
545 | try {
546 | rules = styleSheet != null ? styleSheet.cssRules : void 0;
547 | } catch (e) {
548 |
549 | }
550 | if (!rules) {
551 | return;
552 | }
553 | for (_i = 0, _len = rules.length; _i < _len; _i++) {
554 | rule = rules[_i];
555 | switch (rule.type) {
556 | case CSSRule.IMPORT_RULE:
557 | this.reloadStylesheetImages(rule.styleSheet, path, expando);
558 | break;
559 | case CSSRule.STYLE_RULE:
560 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) {
561 | styleNames = IMAGE_STYLES[_j].styleNames;
562 | this.reloadStyleImages(rule.style, styleNames, path, expando);
563 | }
564 | break;
565 | case CSSRule.MEDIA_RULE:
566 | this.reloadStylesheetImages(rule, path, expando);
567 | }
568 | }
569 | };
570 |
571 | Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) {
572 | var newValue, styleName, value, _i, _len,
573 | _this = this;
574 | for (_i = 0, _len = styleNames.length; _i < _len; _i++) {
575 | styleName = styleNames[_i];
576 | value = style[styleName];
577 | if (typeof value === 'string') {
578 | newValue = value.replace(/\burl\s*\(([^)]*)\)/, function(match, src) {
579 | if (pathsMatch(path, pathFromUrl(src))) {
580 | return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")";
581 | } else {
582 | return match;
583 | }
584 | });
585 | if (newValue !== value) {
586 | style[styleName] = newValue;
587 | }
588 | }
589 | }
590 | };
591 |
592 | Reloader.prototype.reloadStylesheet = function(path) {
593 | var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1,
594 | _this = this;
595 | links = (function() {
596 | var _i, _len, _ref, _results;
597 | _ref = this.document.getElementsByTagName('link');
598 | _results = [];
599 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
600 | link = _ref[_i];
601 | if (link.rel === 'stylesheet' && !link.__LiveReload_pendingRemoval) {
602 | _results.push(link);
603 | }
604 | }
605 | return _results;
606 | }).call(this);
607 | imported = [];
608 | _ref = this.document.getElementsByTagName('style');
609 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
610 | style = _ref[_i];
611 | if (style.sheet) {
612 | this.collectImportedStylesheets(style, style.sheet, imported);
613 | }
614 | }
615 | for (_j = 0, _len1 = links.length; _j < _len1; _j++) {
616 | link = links[_j];
617 | this.collectImportedStylesheets(link, link.sheet, imported);
618 | }
619 | if (this.window.StyleFix && this.document.querySelectorAll) {
620 | _ref1 = this.document.querySelectorAll('style[data-href]');
621 | for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) {
622 | style = _ref1[_k];
623 | links.push(style);
624 | }
625 | }
626 | this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets");
627 | match = pickBestMatch(path, links.concat(imported), function(l) {
628 | return pathFromUrl(_this.linkHref(l));
629 | });
630 | if (match) {
631 | if (match.object.rule) {
632 | this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href);
633 | this.reattachImportedRule(match.object);
634 | } else {
635 | this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object)));
636 | this.reattachStylesheetLink(match.object);
637 | }
638 | } else {
639 | this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one");
640 | for (_l = 0, _len3 = links.length; _l < _len3; _l++) {
641 | link = links[_l];
642 | this.reattachStylesheetLink(link);
643 | }
644 | }
645 | return true;
646 | };
647 |
648 | Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) {
649 | var index, rule, rules, _i, _len;
650 | try {
651 | rules = styleSheet != null ? styleSheet.cssRules : void 0;
652 | } catch (e) {
653 |
654 | }
655 | if (rules && rules.length) {
656 | for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) {
657 | rule = rules[index];
658 | switch (rule.type) {
659 | case CSSRule.CHARSET_RULE:
660 | continue;
661 | case CSSRule.IMPORT_RULE:
662 | result.push({
663 | link: link,
664 | rule: rule,
665 | index: index,
666 | href: rule.href
667 | });
668 | this.collectImportedStylesheets(link, rule.styleSheet, result);
669 | break;
670 | default:
671 | break;
672 | }
673 | }
674 | }
675 | };
676 |
677 | Reloader.prototype.waitUntilCssLoads = function(clone, func) {
678 | var callbackExecuted, executeCallback, poll,
679 | _this = this;
680 | callbackExecuted = false;
681 | executeCallback = function() {
682 | if (callbackExecuted) {
683 | return;
684 | }
685 | callbackExecuted = true;
686 | return func();
687 | };
688 | clone.onload = function() {
689 | console.log("onload!");
690 | _this.knownToSupportCssOnLoad = true;
691 | return executeCallback();
692 | };
693 | if (!this.knownToSupportCssOnLoad) {
694 | (poll = function() {
695 | if (clone.sheet) {
696 | console.log("polling!");
697 | return executeCallback();
698 | } else {
699 | return _this.Timer.start(50, poll);
700 | }
701 | })();
702 | }
703 | return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback);
704 | };
705 |
706 | Reloader.prototype.linkHref = function(link) {
707 | return link.href || link.getAttribute('data-href');
708 | };
709 |
710 | Reloader.prototype.reattachStylesheetLink = function(link) {
711 | var clone, parent,
712 | _this = this;
713 | if (link.__LiveReload_pendingRemoval) {
714 | return;
715 | }
716 | link.__LiveReload_pendingRemoval = true;
717 | if (link.tagName === 'STYLE') {
718 | clone = this.document.createElement('link');
719 | clone.rel = 'stylesheet';
720 | clone.media = link.media;
721 | clone.disabled = link.disabled;
722 | } else {
723 | clone = link.cloneNode(false);
724 | }
725 | clone.href = this.generateCacheBustUrl(this.linkHref(link));
726 | parent = link.parentNode;
727 | if (parent.lastChild === link) {
728 | parent.appendChild(clone);
729 | } else {
730 | parent.insertBefore(clone, link.nextSibling);
731 | }
732 | return this.waitUntilCssLoads(clone, function() {
733 | var additionalWaitingTime;
734 | if (/AppleWebKit/.test(navigator.userAgent)) {
735 | additionalWaitingTime = 5;
736 | } else {
737 | additionalWaitingTime = 200;
738 | }
739 | return _this.Timer.start(additionalWaitingTime, function() {
740 | var _ref;
741 | if (!link.parentNode) {
742 | return;
743 | }
744 | link.parentNode.removeChild(link);
745 | clone.onreadystatechange = null;
746 | return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0;
747 | });
748 | });
749 | };
750 |
751 | Reloader.prototype.reattachImportedRule = function(_arg) {
752 | var href, index, link, media, newRule, parent, rule, tempLink,
753 | _this = this;
754 | rule = _arg.rule, index = _arg.index, link = _arg.link;
755 | parent = rule.parentStyleSheet;
756 | href = this.generateCacheBustUrl(rule.href);
757 | media = rule.media.length ? [].join.call(rule.media, ', ') : '';
758 | newRule = "@import url(\"" + href + "\") " + media + ";";
759 | rule.__LiveReload_newHref = href;
760 | tempLink = this.document.createElement("link");
761 | tempLink.rel = 'stylesheet';
762 | tempLink.href = href;
763 | tempLink.__LiveReload_pendingRemoval = true;
764 | if (link.parentNode) {
765 | link.parentNode.insertBefore(tempLink, link);
766 | }
767 | return this.Timer.start(this.importCacheWaitPeriod, function() {
768 | if (tempLink.parentNode) {
769 | tempLink.parentNode.removeChild(tempLink);
770 | }
771 | if (rule.__LiveReload_newHref !== href) {
772 | return;
773 | }
774 | parent.insertRule(newRule, index);
775 | parent.deleteRule(index + 1);
776 | rule = parent.cssRules[index];
777 | rule.__LiveReload_newHref = href;
778 | return _this.Timer.start(_this.importCacheWaitPeriod, function() {
779 | if (rule.__LiveReload_newHref !== href) {
780 | return;
781 | }
782 | parent.insertRule(newRule, index);
783 | return parent.deleteRule(index + 1);
784 | });
785 | });
786 | };
787 |
788 | Reloader.prototype.generateUniqueString = function() {
789 | return 'livereload=' + Date.now();
790 | };
791 |
792 | Reloader.prototype.generateCacheBustUrl = function(url, expando) {
793 | var hash, oldParams, params, _ref;
794 | if (expando == null) {
795 | expando = this.generateUniqueString();
796 | }
797 | _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params;
798 | if (this.options.overrideURL) {
799 | if (url.indexOf(this.options.serverURL) < 0) {
800 | url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url);
801 | }
802 | }
803 | params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) {
804 | return "" + sep + expando;
805 | });
806 | if (params === oldParams) {
807 | if (oldParams.length === 0) {
808 | params = "?" + expando;
809 | } else {
810 | params = "" + oldParams + "&" + expando;
811 | }
812 | }
813 | return url + params + hash;
814 | };
815 |
816 | return Reloader;
817 |
818 | })();
819 |
820 | }).call(this);
821 |
822 | // livereload
823 | var Connector, LiveReload, Options, Reloader, Timer;
824 |
825 | Connector = __connector.Connector;
826 |
827 | Timer = __timer.Timer;
828 |
829 | Options = __options.Options;
830 |
831 | Reloader = __reloader.Reloader;
832 |
833 | __livereload.LiveReload = LiveReload = (function() {
834 |
835 | function LiveReload(window) {
836 | var _this = this;
837 | this.window = window;
838 | this.listeners = {};
839 | this.plugins = [];
840 | this.pluginIdentifiers = {};
841 | this.console = this.window.location.href.match(/LR-verbose/) && this.window.console && this.window.console.log && this.window.console.error ? this.window.console : {
842 | log: function() {},
843 | error: function() {}
844 | };
845 | if (!(this.WebSocket = this.window.WebSocket || this.window.MozWebSocket)) {
846 | console.error("LiveReload disabled because the browser does not seem to support web sockets");
847 | return;
848 | }
849 | if (!(this.options = Options.extract(this.window.document))) {
850 | console.error("LiveReload disabled because it could not find its own
8 |
9 |
10 | <% end %>
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/spec/rack/livereload/body_processor_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'nokogiri'
3 |
4 | describe Rack::LiveReload::BodyProcessor do
5 | describe 'head tag regex' do
6 | let(:regex) { described_class::HEAD_TAG_REGEX }
7 | subject { regex }
8 |
9 | it { should be_kind_of(Regexp) }
10 |
11 | it 'only picks a valid tag' do
12 | regex.match("").to_s.should eq('')
13 | regex.match("").to_s.should eq('')
14 | regex.match("").to_s.should eq("")
15 | end
16 |
17 | it 'responds false when no head tag' do
18 | regex.match("").should be_falsey
19 | end
20 | end
21 |
22 | let(:processor) { described_class.new(body, options) }
23 | let(:body) { [ page_html ] }
24 | let(:options) { {} }
25 | let(:page_html) { '' }
26 |
27 | let(:processor_result) do
28 | if !processor.processed?
29 | processor.process!(env)
30 | end
31 |
32 | processor
33 | end
34 |
35 | subject { processor }
36 |
37 | describe "livereload local uri" do
38 | context 'does not exist' do
39 | before do
40 | stub_request(:any, 'localhost:35729/livereload.js').to_timeout
41 | end
42 |
43 | it { should use_vendored }
44 | end
45 |
46 | context 'exists' do
47 | before do
48 | stub_request(:any, 'localhost:35729/livereload.js')
49 | end
50 |
51 | it { should_not use_vendored }
52 | end
53 |
54 | context 'with custom port' do
55 | let(:options) { {:live_reload_port => '12348'}}
56 |
57 | context 'exists' do
58 | before do
59 | stub_request(:any, 'localhost:12348/livereload.js')
60 | end
61 | it { should_not use_vendored }
62 | end
63 | end
64 |
65 | context 'specify vendored' do
66 | let(:options) { { :source => :vendored } }
67 |
68 | it { should use_vendored }
69 | end
70 |
71 | context 'specify LR' do
72 | let(:options) { { :source => :livereload } }
73 |
74 | it { should_not use_vendored }
75 | end
76 | end
77 |
78 | context 'text/html' do
79 | before do
80 | processor.stubs(:use_vendored?).returns(true)
81 | end
82 |
83 | let(:host) { 'host' }
84 | let(:env) { { 'HTTP_HOST' => host } }
85 |
86 | let(:processed_body) { processor_result.new_body.join('') }
87 | let(:length) { processor_result.content_length }
88 |
89 | let(:page_html) { '' }
90 |
91 | context 'vendored' do
92 | it 'should add the vendored livereload js script tag' do
93 | processed_body.should include("script")
94 | processed_body.should include(described_class::LIVERELOAD_JS_PATH)
95 |
96 | length.to_s.should == processed_body.length.to_s
97 |
98 | described_class::LIVERELOAD_JS_PATH.should_not include(host)
99 |
100 | processed_body.should include('swfobject')
101 | processed_body.should include('web_socket')
102 | end
103 | end
104 |
105 | context 'at the top of the head tag' do
106 | let(:page_html) { '' }
107 |
108 | let(:body_dom) { Nokogiri::XML(processed_body) }
109 |
110 | it 'should add the livereload js script tag before all other script tags' do
111 | body_dom.at_css("head")[:attribute].should == 'attribute'
112 | body_dom.at_css("script:eq(5)")[:src].should include(described_class::LIVERELOAD_JS_PATH)
113 | body_dom.at_css("script:last-child")[:insert].should == "before"
114 | end
115 |
116 | context 'when a relative URL root is specified' do
117 | before do
118 | ENV['RAILS_RELATIVE_URL_ROOT'] = '/a_relative_path'
119 | end
120 |
121 | it 'should prepend the relative path to the script src' do
122 | body_dom.at_css("script:eq(5)")[:src].should match(%r{^/a_relative_path/})
123 | end
124 | end
125 | end
126 |
127 | describe "LIVERELOAD_PORT value" do
128 | let(:options) { { :live_reload_port => 12345 }}
129 |
130 | it "sets the variable at the top of the file" do
131 | processed_body.should include 'RACK_LIVERELOAD_PORT = 12345'
132 | end
133 | end
134 |
135 | context 'in header tags' do
136 | let(:page_html) { "" }
137 |
138 | let(:body_dom) { Nokogiri::XML(processed_body) }
139 |
140 | it 'should not add the livereload js' do
141 | body_dom.at_css("header")[:class].should == 'hero'
142 | body_dom.css('script').should be_empty
143 | end
144 | end
145 |
146 | context 'not vendored' do
147 | before do
148 | processor.stubs(:use_vendored?).returns(false)
149 | end
150 |
151 | it 'should add the LR livereload js script tag' do
152 | processed_body.should include("script")
153 | processed_body.should include(processor.livereload_local_uri.gsub('localhost', 'host'))
154 | end
155 | end
156 |
157 | context 'set options' do
158 | let(:options) { { :host => new_host, :port => port, :min_delay => min_delay, :max_delay => max_delay } }
159 | let(:min_delay) { 5 }
160 | let(:max_delay) { 10 }
161 | let(:port) { 23 }
162 | let(:new_host) { 'myhost' }
163 |
164 | it 'should add the livereload.js script tag' do
165 | processed_body.should include("mindelay=#{min_delay}")
166 | processed_body.should include("maxdelay=#{max_delay}")
167 | processed_body.should include("port=#{port}")
168 | processed_body.should include("host=#{new_host}")
169 | end
170 | end
171 |
172 | context 'force flash' do
173 | let(:options) { { :force_swf => true } }
174 |
175 | it 'should not add the flash shim' do
176 | processed_body.should include('WEB_SOCKET_FORCE_FLASH')
177 | processed_body.should include('swfobject')
178 | processed_body.should include('web_socket')
179 | end
180 | end
181 |
182 | context 'no flash' do
183 | let(:options) { { :no_swf => true } }
184 |
185 | it 'should not add the flash shim' do
186 | processed_body.should_not include('swfobject')
187 | processed_body.should_not include('web_socket')
188 | end
189 | end
190 |
191 | context 'no host at all' do
192 | let(:env) { {} }
193 |
194 | it 'should use localhost' do
195 | processed_body.should include('localhost')
196 | end
197 | end
198 | end
199 | end
200 |
201 |
--------------------------------------------------------------------------------
/spec/rack/livereload/processing_skip_analyzer_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe Rack::LiveReload::ProcessingSkipAnalyzer do
4 | subject { described_class.new(result, env, options) }
5 |
6 | let(:result) { [ status, headers, body ] }
7 | let(:env) { { 'HTTP_USER_AGENT' => user_agent } }
8 | let(:options) { {} }
9 |
10 | let(:user_agent) { 'Firefox' }
11 | let(:status) { 200 }
12 | let(:headers) { {} }
13 | let(:body) { [] }
14 |
15 | describe '#skip_processing?' do
16 | it "should skip processing" do
17 | subject.skip_processing?.should be_truthy
18 | end
19 | end
20 |
21 | describe '#ignored?' do
22 | let(:options) { { :ignore => [ %r{file} ] } }
23 |
24 | context 'path contains ignore pattern' do
25 | let(:env) { { 'PATH_INFO' => '/this/file', 'QUERY_STRING' => '' } }
26 |
27 | it { should be_ignored }
28 | end
29 |
30 | context 'root path' do
31 | let(:env) { { 'PATH_INFO' => '/', 'QUERY_STRING' => '' } }
32 |
33 | it { should_not be_ignored }
34 | end
35 | end
36 |
37 | describe '#chunked?' do
38 | context 'regular response' do
39 | it { should_not be_chunked }
40 | end
41 |
42 | context 'chunked response' do
43 | let(:headers) { { 'Transfer-Encoding' => 'chunked' } }
44 |
45 | it { should be_chunked }
46 | end
47 | end
48 |
49 | describe '#inline?' do
50 | context 'inline disposition' do
51 | let(:headers) { { 'Content-Disposition' => 'inline; filename=my_inlined_file' } }
52 |
53 | it { should be_inline }
54 | end
55 | end
56 |
57 | describe '#ignored?' do
58 | let(:path_info) { 'path info' }
59 | let(:query_string) { 'query_string' }
60 | let(:env) { { 'PATH_INFO' => path_info, 'QUERY_STRING' => query_string } }
61 |
62 | context 'no ignore set' do
63 | it { should_not be_ignored }
64 | end
65 |
66 | context 'ignore set' do
67 | let(:options) { { :ignore => [ %r{#{path_info}} ] } }
68 |
69 | it { should be_ignored }
70 | end
71 |
72 | context 'ignore set including query_string' do
73 | let(:options) { { :ignore => [ %r{#{path_info}\?#{query_string}} ] } }
74 |
75 | it { should be_ignored }
76 | end
77 | end
78 |
79 | describe '#bad_browser?' do
80 | context 'Firefox' do
81 | it { should_not be_bad_browser }
82 | end
83 |
84 | context 'BAD browser' do
85 | let(:user_agent) { described_class::BAD_USER_AGENTS.first.source }
86 |
87 | it { should be_bad_browser }
88 | end
89 | end
90 |
91 | describe '#html?' do
92 | context 'HTML content' do
93 | let(:headers) { { 'Content-Type' => 'text/html' } }
94 |
95 | it { should be_html }
96 | end
97 |
98 | context 'PDF content' do
99 | let(:headers) { { 'Content-Type' => 'application/pdf' } }
100 |
101 | it { should_not be_html }
102 | end
103 | end
104 |
105 | describe '#get?' do
106 | context 'GET request' do
107 | let(:env) { { 'REQUEST_METHOD' => 'GET' } }
108 |
109 | it { should be_get }
110 | end
111 |
112 | context 'PUT request' do
113 | let(:env) { { 'REQUEST_METHOD' => 'PUT' } }
114 |
115 | it { should_not be_get }
116 | end
117 |
118 | context 'POST request' do
119 | let(:env) { { 'REQUEST_METHOD' => 'POST' } }
120 |
121 | it { should_not be_get }
122 | end
123 |
124 | context 'DELETE request' do
125 | let(:env) { { 'REQUEST_METHOD' => 'DELETE' } }
126 |
127 | it { should_not be_get }
128 | end
129 |
130 | context 'PATCH request' do
131 | let(:env) { { 'REQUEST_METHOD' => 'PATCH' } }
132 |
133 | it { should_not be_get }
134 | end
135 | end
136 | end
137 |
138 |
--------------------------------------------------------------------------------
/spec/rack/livereload_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'nokogiri'
3 |
4 | describe Rack::LiveReload do
5 | let(:middleware) { described_class.new(app, options) }
6 | let(:app) { stub }
7 |
8 | subject { middleware }
9 |
10 | it 'should be an app' do
11 | middleware.app.should be == app
12 | end
13 |
14 | let(:env) { {} }
15 | let(:options) { {} }
16 |
17 | context '/__rack/livereload.js' do
18 | let(:env) { { 'PATH_INFO' => described_class::BodyProcessor::LIVERELOAD_JS_PATH } }
19 |
20 | before do
21 | middleware.expects(:deliver_file).returns(true)
22 | end
23 |
24 | it 'should return the js file' do
25 | middleware._call(env).should be_truthy
26 | end
27 | end
28 | end
29 |
30 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'mocha/api'
2 | require 'webmock/rspec'
3 |
4 | require 'rack-livereload'
5 |
6 | RSpec.configure do |c|
7 | c.mock_with :mocha
8 | end
9 |
10 | module RSpec::Matchers
11 | define :use_vendored do
12 | match do |subject|
13 | subject.use_vendored?
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------