diff --git a/README.md b/README.md index 3411883..172bdd3 100644 --- a/README.md +++ b/README.md @@ -61,22 +61,6 @@ Will compile to: } } -#Building & Testing retina.js - -##How to build - -We use [Coyote](http://imulus.github.com/coyote/) to stitch everything together. Install Coyote by running `gem install coyote`. - - $ rake build - -This will compile the CoffeeScript and put the new build in `build/retina.js` - -During development, you can use: - - $ rake watch - -to keep an eye on the source files and automatically compile them to `test/functional/public`. Handy for testing in the browser. - ##How to test We use [mocha](http://visionmedia.github.com/mocha/) for unit testing with [should](https://github.com/visionmedia/should.js) assertions. Install mocha and should by running `npm install -g mocha should`. @@ -85,19 +69,24 @@ To run the test suite: $ mocha -We also have a [Sinatra](http://sinatrarb.com) app for testing in the browser locally. This is handy for testing on your retina devices. +Use [http-server](https://github.com/nodeapps/http-server) for node.js to test it. + +If you've updated `retina.js` be sure to copy it from `src/retina.js` to `test/functional/public/retina.js`. To start the server, run: - $ cd test/functional && ruby app.rb + $ cd test/functional && http-server Then navigate your browser to [http://localhost:4567](http://localhost:4567) +After that, open up `test/functional/public/index.html` in your editor, and try commenting out the line that spoofs retina support, and reloading it. #License (MIT License) +Copyright (C) 2012 Ben Atkin + Copyright (C) 2012 [Imulus](http://imulus.com) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/Rakefile b/Rakefile deleted file mode 100644 index 137e7b6..0000000 --- a/Rakefile +++ /dev/null @@ -1,35 +0,0 @@ -require 'fileutils' -require 'zip/zip' -require 'zip/zipfilesystem' -require 'coyote' -require 'coyote/rake' - -task :build do - FileUtils.mkdir_p "build" - Coyote.run "src/retina.coffee", "build/retina.js", :compress => true - FileUtils.mkdir_p "test/functional/public" - Coyote.run "src/retina.coffee", "test/functional/public/retina.js" -end - -coyote :watch do |config| - config.input = "src/retina.coffee" - config.output = "test/functional/public/retina.js" -end - -task :package => [:build] do - tmp = "tmp/#{Time.now.to_i}" - zip = "pkg/retina.zip" - - FileUtils.rm zip, :force => true - FileUtils.mkdir_p tmp - FileUtils.cp "README.md", "#{tmp}/README.md" - FileUtils.cp "src/retina.less", "#{tmp}/retina.less" - FileUtils.cp "build/retina.js", "#{tmp}/retina.js" - - Zip::ZipFile.open(zip, 'w') do |zipfile| - Dir["#{tmp}/**/**"].reject { |f| f == zip }.each do |file| - zipfile.add(file.sub(tmp+'/',''),file) - end - end -end - diff --git a/src/retina.coffee b/src/retina.coffee deleted file mode 100644 index a9116d5..0000000 --- a/src/retina.coffee +++ /dev/null @@ -1,14 +0,0 @@ -# retina.coffee -# Source for retina.js, a high-resolution image swapper (http://www.retinajs.com) - -#= require retina_image.coffee - - -# First check that we're on a retina device. -# We don't even want this guy executing if we're not. -# Bind everything to the window object's onload. -# This helps wait for the images to get loaded into the DOM. - -if window.devicePixelRatio > 1 - window.onload = -> - new RetinaImage(image) for image in document.getElementsByTagName("img") diff --git a/src/retina.js b/src/retina.js new file mode 100644 index 0000000..efcdc8e --- /dev/null +++ b/src/retina.js @@ -0,0 +1,114 @@ +// retina.js +// Source for retina.js, a high-resolution image swapper (http://www.retinajs.com) + +(function() { + +var root = (typeof exports == 'undefined' ? window : exports); + +function RetinaImagePath(path) { + this.path = path; + // Split the file extension off the image path, + // and put it back together with @2x before the extension. + // "/path/to/image.png" => "/path/to/image@2x.png" + var path_segments = this.path.split('.'); + var path_without_extension = path_segments.slice(0, (path_segments.length - 1)).join("."); + var extension = path_segments[path_segments.length - 1]; + this.at_2x_path = path_without_extension + "@2x." + extension; +} + +root.RetinaImagePath = RetinaImagePath; + +// Class variable [Array] +// cache of files we've checked on the server +RetinaImagePath.confirmed_paths = []; + +// Function to test if image is external +RetinaImagePath.prototype.is_external = function() { + return !!(this.path.match(/^https?\:/i) && !this.path.match('//' + document.domain) ) +} + +RetinaImagePath.prototype.has_2x_variant = function() { + var http; + if (this.is_external()) { + // If the image path is on an external server, + // exit early to avoid cross-domain ajax errors + return false; + } else if (this.at_2x_path in RetinaImagePath.confirmed_paths) { + // If we have already checked and confirmed that + // the @2x variant exists, then just return true + return true + } else { + // Otherwise, prepare an AJAX request for the HEAD only. + // We don't need a full request because we're only + // checking to see if the @2x version exists on the server + http = new XMLHttpRequest; + http.open('HEAD', this.at_2x_path, false); + http.send(); + + // If we get an A-OK from the server, + // push file path onto array of confirmed files + if (http.status >= 200 && http.status <= 399) { + RetinaImagePath.confirmed_paths.push(this.at_2x_path); + return true; + } else { + return false; + } + } +} + +function RetinaImage(el) { + this.el = el; + this.path = new RetinaImagePath(this.el.getAttribute('src')); + if (this.path.has_2x_variant()) { + this.swap(); + } +} + +root.RetinaImage = RetinaImage; + +// Method for swapping in a new image path +// Applies the dimensions of the existing image +// to the image with the new image path +RetinaImage.prototype.swap = function(path) { + if (typeof path == 'undefined') path = this.path.at_2x_path; + + // We wrap this in a named self-executer so we can reference + // it in a setTimeout if the image has not loaded yet. + var that = this; + function load() { + if (! that.el.complete) { + // Check that the image has loaded. + // We need to wait for the image to load to grab proper dimensions. + + // If it has not, try again in a few milliseconds. + // We've found 5ms to be the fastest we can crank this up + // and still have the script detect image loads reliably and efficiently. + setTimeout(load, 5); + } else { + // Once the the image has loaded we know we + // can grab the proper dimensions of the original image + // and go ahead and swap in the new image path and apply the dimensions + that.el.setAttribute('width', that.el.offsetWidth); + that.el.setAttribute('height', that.el.offsetHeight); + that.el.setAttribute('src', path); + } + } + load(); +} + +// First check that we're on a retina device. +// We don't even want this guy executing if we're not. +// Bind everything to the window object's onload. +// This helps wait for the images to get loaded into the DOM. + +if (root.devicePixelRatio > 1) { + window.onload = function() { + var images = document.getElementsByTagName("img"), retinaImages = [], i, image; + for (i = 0; i < images.length; i++) { + image = images[i]; + retinaImages.push(new RetinaImage(image)); + } + } +} + +})(); diff --git a/src/retina_image.coffee b/src/retina_image.coffee deleted file mode 100644 index 5252cb1..0000000 --- a/src/retina_image.coffee +++ /dev/null @@ -1,39 +0,0 @@ -#= require retina_image_path.coffee - -class RetinaImage - - constructor: (@el) -> - @path = new RetinaImagePath(@el.getAttribute('src')) - @swap() if @path.has_2x_variant() - - - - # Method for swapping in a new image path - # Applies the dimensions of the existing image - # to the image with the new image path - swap: (path = @path.at_2x_path) -> - - # We wrap this in a named self-executer so we can reference - # it in a setTimeout if the image has not loaded yet. - do load = => - - # Check that the image has loaded. - # We need to wait for the image to load to grab proper dimensions. - unless @el.complete - - # If it has not, try again in a few milliseconds. - # We've found 5ms to be the fastest we can crank this up - # and still have the script detect image loads reliably and efficiently. - setTimeout load, 5 - - # Once the the image has loaded we know we - # can grab the proper dimensions of the original image - # and go ahead and swap in the new image path and apply the dimensions - else - @el.setAttribute('width', @el.offsetWidth) - @el.setAttribute('height', @el.offsetHeight) - @el.setAttribute('src', path) - - -root = exports ? window -root.RetinaImage = RetinaImage \ No newline at end of file diff --git a/src/retina_image_path.coffee b/src/retina_image_path.coffee deleted file mode 100644 index 8be8d74..0000000 --- a/src/retina_image_path.coffee +++ /dev/null @@ -1,51 +0,0 @@ -class RetinaImagePath - - # Class variable [Array] - # cache of files we've checked on the server - @confirmed_paths = [] - - - constructor: (@path) -> - # Split the file extension off the image path, - # and put it back together with @2x before the extension. - # "/path/to/image.png" => "/path/to/image@2x.png" - path_segments = @path.split('.') - path_without_extension = path_segments.slice(0, (path_segments.length - 1)).join(".") - extension = path_segments[path_segments.length - 1] - @at_2x_path = "#{path_without_extension}@2x.#{extension}" - - - # Function to test if image is external - is_external: -> - !!( @path.match(/^https?\:/i) and !@path.match('//' + document.domain) ) - - has_2x_variant: -> - # If the image path is on an external server, - # exit early to avoid cross-domain ajax errors - if @is_external() - return false - - # If we have already checked and confirmed that - # the @2x variant exists, then just return true - else if @at_2x_path in RetinaImagePath.confirmed_paths - return true - - # Otherwise, prepare an AJAX request for the HEAD only. - # We don't need a full request because we're only - # checking to see if the @2x version exists on the server - else - http = new XMLHttpRequest - http.open('HEAD', @at_2x_path, false) - http.send() - - # If we get an A-OK from the server, - # push file path onto array of confirmed files - if http.status in [200..399] - RetinaImagePath.confirmed_paths.push @at_2x_path - return true - else - return false - - -root = exports ? window -root.RetinaImagePath = RetinaImagePath \ No newline at end of file diff --git a/test/fixtures/image.coffee b/test/fixtures/image.coffee deleted file mode 100644 index 3ce7548..0000000 --- a/test/fixtures/image.coffee +++ /dev/null @@ -1,19 +0,0 @@ -class Image - - constructor: -> - @complete = true - @attributes = - src : "/images/some_image.png" - offsetWidth : 500 - offsetHeight : 400 - - setAttribute: (name, value) -> - @attributes[name] = value - - getAttribute: (name) -> - return @attributes[name] - - - -root = exports ? window -root.Image = Image \ No newline at end of file diff --git a/test/fixtures/image.js b/test/fixtures/image.js new file mode 100644 index 0000000..976b56b --- /dev/null +++ b/test/fixtures/image.js @@ -0,0 +1,19 @@ +function Image() { + this.complete = true; + this.attributes = { + src : "/images/some_image.png", + offsetWidth : 500, + offsetHeight : 400 + }; +} + +Image.prototype.setAttribute = function(name, value) { + this.attributes[name] = value; +} + +Image.prototype.getAttribute = function(name) { + return this.attributes[name]; +} + +var root = (exports || window); +root.Image = Image; diff --git a/test/fixtures/xml_http_request.coffee b/test/fixtures/xml_http_request.coffee deleted file mode 100644 index e2af75e..0000000 --- a/test/fixtures/xml_http_request.coffee +++ /dev/null @@ -1,12 +0,0 @@ -class XMLHttpRequest - @status = 200 - - constructor: -> - @status = XMLHttpRequest.status - - open: -> true - - send: -> true - -root = exports ? window -root.XMLHttpRequest = XMLHttpRequest \ No newline at end of file diff --git a/test/fixtures/xml_http_request.js b/test/fixtures/xml_http_request.js new file mode 100644 index 0000000..41e1772 --- /dev/null +++ b/test/fixtures/xml_http_request.js @@ -0,0 +1,16 @@ +function XMLHttpRequest() { + this.status = XMLHttpRequest.status; +} + +XMLHttpRequest.status = 200; + +XMLHttpRequest.prototype.open = function() { + return true; +} + +XMLHttpRequest.prototype.send = function() { + return true; +} + +var root = (exports || window); +root.XMLHttpRequest = XMLHttpRequest; diff --git a/test/functional/app.rb b/test/functional/app.rb deleted file mode 100644 index b4517c2..0000000 --- a/test/functional/app.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'sinatra' - -get '/' do - erb :index -end \ No newline at end of file diff --git a/test/functional/views/index.erb b/test/functional/public/index.html similarity index 85% rename from test/functional/views/index.erb rename to test/functional/public/index.html index 5bf18e1..fafd250 100644 --- a/test/functional/views/index.erb +++ b/test/functional/public/index.html @@ -16,7 +16,9 @@ @@ -34,10 +36,3 @@ - - - - - - - diff --git a/test/mocha.opts b/test/mocha.opts index fafd834..b1d9b9f 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,5 +1,4 @@ --require should --reporter spec ---compilers coffee:coffee-script --slow 20 ---growl \ No newline at end of file +--growl diff --git a/test/retina_image_path.test.coffee b/test/retina_image_path.test.coffee deleted file mode 100644 index 5473820..0000000 --- a/test/retina_image_path.test.coffee +++ /dev/null @@ -1,108 +0,0 @@ -# Create a document object because we don't have one -# in our Node test environment -global.document = {domain: null} -global.Image = require('./fixtures/image').Image -global.XMLHttpRequest = require('./fixtures/xml_http_request').XMLHttpRequest -global.RetinaImage = require('../src/retina_image').RetinaImage -global.RetinaImagePath = require('../src/retina_image_path').RetinaImagePath - - -describe 'RetinaImagePath', -> - path = null - - describe '#is_external()', -> - it 'should return true when image path references a remote domain with www', -> - document.domain = "www.apple.com" - path = new RetinaImagePath("http://www.google.com/images/some_image.png") - path.is_external().should.equal true - - - it 'should return true when image path references a remote domain without www', -> - document.domain = "www.apple.com" - path = new RetinaImagePath("http://google.com/images/some_image.png") - path.is_external().should.equal true - - - it 'should return true when image path references a remote domain with https', -> - document.domain = "www.apple.com" - path = new RetinaImagePath("https://google.com/images/some_image.png") - path.is_external().should.equal true - - - it 'should return true when image path is a remote domain with www and domain is localhost', -> - document.domain = "localhost" - path = new RetinaImagePath("http://www.google.com/images/some_image.png") - path.is_external().should.equal true - - - it 'should return true when image path is a remote domain without www and domain is localhost', -> - document.domain = "localhost" - path = new RetinaImagePath("http://google.com/images/some_image.png") - path.is_external().should.equal true - - it 'should return true when image path has www and domain does not', -> - document.domain = "apple.com" - path = new RetinaImagePath("http://www.apple.com/images/some_image.png") - path.is_external().should.equal true - - - it 'should return true when image path does not have www and domain does', -> - document.domain = "www.apple.com" - path = new RetinaImagePath("http://apple.com/images/some_image.png") - path.is_external().should.equal true - - - it 'should return false when image path is relative with www', -> - document.domain = "www.apple.com" - path = new RetinaImagePath("/images/some_image.png") - path.is_external().should.equal false - - - it 'should return false when image path is relative without www', -> - document.domain = "apple.com" - path = new RetinaImagePath("/images/some_image.png") - path.is_external().should.equal false - - - it 'should return false when image path is relative to localhost', -> - document.domain = "localhost" - path = new RetinaImagePath("/images/some_image.png") - path.is_external().should.equal false - - - it 'should return false when image path has same domain as current site with www', -> - document.domain = "www.apple.com" - path = new RetinaImagePath("http://www.apple.com/images/some_image.png") - path.is_external().should.equal false - - - describe '#has_2x_variant()', -> - it 'should return false when #is_external() is true', -> - document.domain = "www.apple.com" - path = new RetinaImagePath("http://google.com/images/some_image.png") - path.has_2x_variant().should.equal false - - - it 'should return false when remote at2x image does not exist', -> - XMLHttpRequest.status = 404 # simulate a failing request - path = new RetinaImagePath("/images/some_image.png") - path.has_2x_variant().should.equal false - - - it 'should return true when remote at2x image exists', -> - XMLHttpRequest.status = 200 # simulate a proper request - path = new RetinaImagePath("/images/some_image.png") - path.has_2x_variant().should.equal true - - - it 'should add path to cache when at2x image exists', -> - XMLHttpRequest.status = 200 # simulate a proper request - path = new RetinaImagePath("/images/some_image.png") - path.has_2x_variant() - RetinaImagePath.confirmed_paths.should.include path.at_2x_path - - - it 'should return true when the at2x image path has already been checked and confirmed', -> - RetinaImagePath.confirmed_paths = ['/images/some_image@2x.png'] - path = new RetinaImagePath("/images/some_image.png") - path.has_2x_variant().should.equal true diff --git a/test/retina_image_path.test.js b/test/retina_image_path.test.js new file mode 100644 index 0000000..d6d8693 --- /dev/null +++ b/test/retina_image_path.test.js @@ -0,0 +1,113 @@ +// Create a document object because we don't have one +// in our Node test environment +global.document = {domain: null}; +global.Image = require('./fixtures/image').Image; +global.XMLHttpRequest = require('./fixtures/xml_http_request').XMLHttpRequest; +global.RetinaImage = require('../src/retina').RetinaImage; +global.RetinaImagePath = require('../src/retina').RetinaImagePath; + + +describe('RetinaImagePath', function() { + var path = null; + + describe('#is_external()', function() { + it('should return true when image path references a remote domain with www', function() { + document.domain = "www.apple.com"; + path = new RetinaImagePath("http://www.google.com/images/some_image.png"); + path.is_external().should.equal(true); + }); + + it('should return true when image path references a remote domain without www', function() { + document.domain = "www.apple.com"; + path = new RetinaImagePath("http://google.com/images/some_image.png"); + path.is_external().should.equal(true); + }); + + it('should return true when image path references a remote domain with https', function() { + document.domain = "www.apple.com"; + path = new RetinaImagePath("https://google.com/images/some_image.png"); + path.is_external().should.equal(true); + }); + + it('should return true when image path is a remote domain with www and domain is localhost', function() { + document.domain = "localhost"; + path = new RetinaImagePath("http://www.google.com/images/some_image.png"); + path.is_external().should.equal(true); + }); + + it('should return true when image path is a remote domain without www and domain is localhost', function() { + document.domain = "localhost" + path = new RetinaImagePath("http://google.com/images/some_image.png") + path.is_external().should.equal(true); + }); + + it('should return true when image path has www and domain does not', function() { + document.domain = "apple.com"; + path = new RetinaImagePath("http://www.apple.com/images/some_image.png"); + path.is_external().should.equal(true); + }); + + it('should return true when image path does not have www and domain does', function() { + document.domain = "www.apple.com"; + path = new RetinaImagePath("http://apple.com/images/some_image.png"); + path.is_external().should.equal(true); + }); + + it('should return false when image path is relative with www', function() { + document.domain = "www.apple.com"; + path = new RetinaImagePath("/images/some_image.png"); + path.is_external().should.equal(false); + }); + + it('should return false when image path is relative without www', function() { + document.domain = "apple.com"; + path = new RetinaImagePath("/images/some_image.png"); + path.is_external().should.equal(false); + }); + + it('should return false when image path is relative to localhost', function() { + document.domain = "localhost"; + path = new RetinaImagePath("/images/some_image.png"); + path.is_external().should.equal(false); + }); + + it('should return false when image path has same domain as current site with www', function() { + document.domain = "www.apple.com"; + path = new RetinaImagePath("http://www.apple.com/images/some_image.png"); + path.is_external().should.equal(false); + }); + }); + + describe('#has_2x_variant()', function() { + it('should return false when #is_external() is true', function() { + document.domain = "www.apple.com"; + path = new RetinaImagePath("http://google.com/images/some_image.png"); + path.has_2x_variant().should.equal(false); + }); + + it('should return false when remote at2x image does not exist', function() { + XMLHttpRequest.status = 404; // simulate a failing request + path = new RetinaImagePath("/images/some_image.png"); + path.has_2x_variant().should.equal(false); + }); + + it('should return true when remote at2x image exists', function() { + XMLHttpRequest.status = 200; // simulate a proper request + path = new RetinaImagePath("/images/some_image.png"); + path.has_2x_variant().should.equal(true); + }); + + it('should add path to cache when at2x image exists', function() { + XMLHttpRequest.status = 200; // simulate a proper request + path = new RetinaImagePath("/images/some_image.png"); + path.has_2x_variant(); + RetinaImagePath.confirmed_paths.should.include(path.at_2x_path); + }); + + it('should return true when the at2x image path has already been checked and confirmed', function() { + RetinaImagePath.confirmed_paths = ['/images/some_image@2x.png'] + path = new RetinaImagePath("/images/some_image.png") + path.has_2x_variant().should.equal(true); + }); + }); +});