diff --git a/.gitignore b/.gitignore index 50880aa7..251f2cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -test/public/vendor/rails.js +test/public/vendor/jquery.js +node_modules *.swp *.swo .#* diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000..9bfd6603 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,71 @@ +{ + "maxerr" : 50, // {int} Maximum error before stopping + + // Enforcing + "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) + "camelcase" : true, // true: Identifiers must be in camelCase + "curly" : false, // true: Require {} for every new block or scope + "eqeqeq" : true, // true: Require triple equals (===) for comparison + "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() + "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. + "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` + "indent" : 2, // {int} Number of spaces to use for indentation + "latedef" : false, // true: Require variables/functions to be defined before being used + "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` + "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` + "noempty" : true, // true: Prohibit use of empty blocks + "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. + "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) + "plusplus" : false, // true: Prohibit use of `++` & `--` + "quotmark" : "single", // Quotation mark consistency: + // false : do nothing (default) + // true : ensure whatever is used is consistent + // "single" : require single quotes + // "double" : require double quotes + "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) + "unused" : true, // Unused variables: + // true : all variables, last function parameter + // "vars" : all variables only + // "strict" : all variables, all function parameters + "strict" : true, // true: Requires all functions run in ES5 Strict Mode + "maxparams" : false, // {int} Max number of formal params allowed per function + "maxdepth" : false, // {int} Max depth of nested blocks (within functions) + "maxstatements" : false, // {int} Max number statements per function + "maxcomplexity" : false, // {int} Max cyclomatic complexity per function + "maxlen" : false, // {int} Max number of characters per line + + // Relaxing + "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) + "boss" : false, // true: Tolerate assignments where comparisons would be expected + "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. + "eqnull" : false, // true: Tolerate use of `== null` + "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) + "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) + "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) + // (ex: `for each`, multiple try/catch, function expression…) + "evil" : false, // true: Tolerate use of `eval` and `new Function()` + "expr" : false, // true: Tolerate `ExpressionStatement` as Programs + "funcscope" : false, // true: Tolerate defining variables inside control statements + "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') + "iterator" : false, // true: Tolerate using the `__iterator__` property + "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block + "laxbreak" : false, // true: Tolerate possibly unsafe line breakings + "laxcomma" : false, // true: Tolerate comma-first style coding + "loopfunc" : false, // true: Tolerate functions being defined in loops + "multistr" : false, // true: Tolerate multi-line strings + "noyield" : false, // true: Tolerate generator functions with no yield statement in them. + "notypeof" : false, // true: Tolerate invalid typeof operator values + "proto" : false, // true: Tolerate using the `__proto__` property + "scripturl" : false, // true: Tolerate script-targeted URLs + "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` + "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation + "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` + "validthis" : false, // true: Tolerate using this in a non-constructor function + + // Environments + "browser" : true, // Web Browser (window, document, etc) + "jquery" : true, + "devel" : true, // Development/debugging (alert, confirm, etc) + + "globals" : {} +} diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..3af83847 --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +script/ +test/ +Rakefile +Gemfile* diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..7e6cb080 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +language: ruby +sudo: false +cache: + - bundler + - directories: + - $HOME/.npm +script: ./script/cibuild +before_install: + - "npm install jshint -g" +env: + - JQUERY_VERSION: 1.8.0 + - JQUERY_VERSION: 1.8.1 + - JQUERY_VERSION: 1.8.2 + - JQUERY_VERSION: 1.8.3 + - JQUERY_VERSION: 1.9.0 + - JQUERY_VERSION: 1.9.1 + - JQUERY_VERSION: 1.10.0 + - JQUERY_VERSION: 1.10.1 + - JQUERY_VERSION: 1.10.2 + - JQUERY_VERSION: 1.11.0 + - JQUERY_VERSION: 1.11.1 + - JQUERY_VERSION: 1.11.2 + - JQUERY_VERSION: 1.12.0 + - JQUERY_VERSION: 2.0.0 + - JQUERY_VERSION: 2.1.0 + - JQUERY_VERSION: 2.1.1 + - JQUERY_VERSION: 2.1.2 + - JQUERY_VERSION: 2.1.3 + - JQUERY_VERSION: 2.2.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..6827438d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,109 @@ +Contributing to jquery-ujs +===================== + +[![Build Status](https://travis-ci.org/rails/jquery-ujs.svg?branch=master)](https://travis-ci.org/rails/jquery-ujs) + +jquery-ujs is work of [many contributors](https://github.com/rails/jquery-ujs/graphs/contributors). You're encouraged to submit [pull requests](https://github.com/rails/jquery-ujs/pulls), [propose features and discuss issues](https://github.com/rails/jquery-ujs/issues). + +#### Fork the Project + +Fork the [project on Github](https://github.com/rails/jquery-ujs) and check out your copy. + +``` +git clone https://github.com/contributor/jquery-ujs.git +cd jquery-ujs +git remote add upstream https://github.com/rails/jquery-ujs.git +``` + +#### Create a Topic Branch + +Make sure your fork is up-to-date and create a topic branch for your feature or bug fix. + +``` +git checkout master +git pull upstream master +git checkout -b my-feature-branch +``` + +#### Bundle Install and Test + +Ensure that you can build the project and run tests. Run `rake test:server` first, and then run the web tests by visiting [[http://localhost:4567]] in your browser. Click the version links at the top right of the page to run the test suite with the different supported versions of jQuery. + +``` +bundle install +rake test:server +``` + +#### Write Tests + +Try to write a test that reproduces the problem you're trying to fix or describes a feature that you want to build. Add to [test](test). + +Here are some additional notes to keep in mind when developing your patch for jquery-ujs. + +* The tests can be found in: +``` + |~test + |~public + |~test +``` +* Some tests ensure consistent behavior across the major browsers, meaning it is possible for tests to pass in Firefox, but fail in Internet Explorer. If possible, it helps if you can run the test suite in multiple browsers before submitting patches. + +We definitely appreciate pull requests that highlight or reproduce a problem, even without a fix. + +#### Write Code + +Implement your feature or bug fix. + +Make sure that `bundle exec rake test` completes without errors. + +#### Write Documentation + +Document any external behavior in the [README](README.md). + +#### Commit Changes + +Make sure git knows your name and email address: + +``` +git config --global user.name "Your Name" +git config --global user.email "contributor@example.com" +``` + +Writing good commit logs is important. A commit log should describe what changed and why. + +``` +git add ... +git commit +``` + +#### Push + +``` +git push origin my-feature-branch +``` + +#### Make a Pull Request + +Go to https://github.com/contributor/jquery-ujs and select your feature branch. Click the 'Pull Request' button and fill out the form. Pull requests are usually reviewed within a few days. + +#### Rebase + +If you've been working on a change for a while, rebase with upstream/master. + +``` +git fetch upstream +git rebase upstream/master +git push origin my-feature-branch -f +``` + +#### Check on Your Pull Request + +Go back to your pull request after a few minutes and see whether it passed muster with Travis-CI. Everything should look green, otherwise fix issues and amend your commit as described above. + +#### Be Patient + +It's likely that your change will not be merged and that the nitpicky maintainers will ask you to do more, or fix seemingly benign problems. Hang on there! + +#### Thank You + +Please do know that we really appreciate and value your time and work. We love you, really. diff --git a/Gemfile b/Gemfile index a1767e96..aca39a6b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,2 +1,6 @@ -gem "json" -gem "sinatra", "= 1.0" +source 'https://rubygems.org' + +gem 'sinatra', '~> 1.0' +gem 'shotgun', :group => :reloadable +gem 'thin', :group => :reloadable +gem 'rake' diff --git a/Gemfile.lock b/Gemfile.lock index 6e7745e3..8ced3297 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,13 +1,29 @@ GEM + remote: https://rubygems.org/ specs: - json (1.4.6) - rack (1.2.1) - sinatra (1.0) + daemons (1.1.9) + eventmachine (1.0.4) + rack (1.6.0) + rack-protection (1.5.3) + rack + rake (10.4.2) + shotgun (0.9) rack (>= 1.0) + sinatra (1.4.5) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (~> 1.3, >= 1.3.4) + thin (1.6.3) + daemons (~> 1.0, >= 1.0.9) + eventmachine (~> 1.0) + rack (~> 1.0) + tilt (1.4.1) PLATFORMS ruby DEPENDENCIES - json - sinatra (= 1.0) + rake + shotgun + sinatra (~> 1.0) + thin diff --git a/MIT-LICENSE.txt b/MIT-LICENSE similarity index 94% rename from MIT-LICENSE.txt rename to MIT-LICENSE index ed37a233..9c7e3c0b 100644 --- a/MIT-LICENSE.txt +++ b/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2007-2010 Contributors at http://github.com/rails/jquery-ujs/contributors +Copyright (c) 2007-2016 Contributors at http://github.com/rails/jquery-ujs/contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md new file mode 100644 index 00000000..2e429bd1 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +Unobtrusive scripting adapter for jQuery +======================================== + +This unobtrusive scripting support file is developed for the Ruby on Rails framework, but is not strictly tied to any specific backend. You can drop this into any application to: + +- force confirmation dialogs for various actions; +- make non-GET requests from hyperlinks; +- make forms or hyperlinks submit data asynchronously with Ajax; +- have submit buttons become automatically disabled on form submit to prevent double-clicking. + +These features are achieved by adding certain ["data" attributes][data] to your HTML markup. In Rails, they are added by the framework's template helpers. + +Full [documentation is on the wiki][wiki], including the [list of published Ajax events][events]. + +Requirements +------------ + +- [jQuery 1.8.x or higher][jquery]; +- HTML5 doctype (optional). + +If you don't use HTML5, adding "data" attributes to your HTML4 or XHTML pages might make them fail [W3C markup validation][validator]. However, this shouldn't create any issues for web browsers or other user agents. + +Installation using the jquery-rails gem +------------ + +For automated installation in Rails, use the "jquery-rails" gem. Place this in your Gemfile: + +```ruby +gem 'jquery-rails' +``` + +And run: + +```shell +$ bundle install +``` + +Require both `jquery` and `jquery_ujs` into your application.js manifest. + +```javascript +//= require jquery +//= require jquery_ujs +``` + +Installation using npm. +------------ + +Run `npm install --save jquery-ujs` to install the jquery-ujs package. + +Installation using Rails and Webpacker +------------ + +If you're using [webpacker](https://github.com/rails/webpacker) (introduced in [Rails 5.1](http://edgeguides.rubyonrails.org/5_1_release_notes.html#optional-webpack-support)) to manage JavaScript assets, then you can add the jquery-ujs npm package to your project using the [yarn](https://yarnpkg.com/en/) CLI. + +``` +$ yarn add jquery-ujs +``` + +Then, from any of your included files (e.g. `app/javascript/packs/application.js`, or from a JavaScript file imported by such a pack), you need only import the package for jquery-ujs to be initialized: + +```js +import {} from 'jquery-ujs' +``` + +Installation using Bower +------------ + +Run `bower install jquery-ujs --save` to install the jquery-ujs package. + +Usage +------------ + +Require both `jquery` and `jquery-ujs` into your application.js manifest. + +```javascript +//= require jquery +//= require jquery-ujs +``` + +How to run tests +------------ + +Follow [this wiki](https://github.com/rails/jquery-ujs/wiki/Running-Tests-and-Contributing) to run tests. + +## Contributing to jquery-ujs + +jquery-ujs is work of many contributors. You're encouraged to submit pull requests, propose +features and discuss issues. + +See [CONTRIBUTING](CONTRIBUTING.md). + +## License +jquery-ujs is released under the [MIT License](MIT-LICENSE). + +[data]: http://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes "Embedding custom non-visible data with the data-* attributes" +[wiki]: https://github.com/rails/jquery-ujs/wiki +[events]: https://github.com/rails/jquery-ujs/wiki/ajax +[jquery]: http://docs.jquery.com/Downloading_jQuery +[validator]: http://validator.w3.org/ +[csrf]: http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html +[adapter]: https://github.com/rails/jquery-ujs/raw/master/src/rails.js diff --git a/README.rdoc b/README.rdoc deleted file mode 100644 index 1ed16791..00000000 --- a/README.rdoc +++ /dev/null @@ -1,65 +0,0 @@ -= jquery-ujs - -Unobtrusive jQuery with Rails 3 - -== The rails.js file from master branch supports following versions of jQuery: - -* 1.4.3 -* 1.4.4 - -== If you are using one of the following version of jQuery then use branch v1.4 . - -* 1.4 -* 1.4.1 -* 1.4.2 - -rails.js file from v1.4 branch can be accessed at https://github.com/rails/jquery-ujs/blob/v1.4/src/rails.js . - - -== Automated Installation - -=== Step 1 - -Add this line to your Gemfile: - - gem 'jquery-rails' - -=== Step 2 - -Run this command: - - $ rails generate jquery:install # --ui if you want jQuery UI - - -== Manual installation - -=== Step 1 - -Download jQuery from http://docs.jquery.com/Downloading_jQuery and put the file in public/javascripts. For example, the file might look like: - - public/javascripts/jquery-1.4.4.min.js - -=== Step 2 - -Copy rails.js from http://github.com/rails/jquery-ujs/raw/master/src/rails.js into public/javascripts - overwriting the prototype one (you can also delete the other prototype files if you don't need them for anything else.) - -=== Step 3 (optional) - -Switch the javascript_include_tag :defaults to use jquery instead of the default prototype helpers. Uncomment following line from file config/application.rb - - config.action_view.javascript_expansions[:defaults] = %w(jquery rails application) - -= Testing - -== Installation - - $ gem install bundler - $ bundle install - -== Running tests - - $ bundle exec ruby test/server.rb - -Visit http://localhost:4567 and all the tests should pass. - -At the top of the page you will see links to jQuery 1.4.3 and 1.4.4 . By clicking on those links you will be executing the tests against the clicked version of jquery. By default test uses jQuery 1.4.4 . diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..59316f77 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,23 @@ +## Releasing jquery-ujs + +### Releasing to npm + +Make sure npm's configuration `sign-git-tag` is set to true. + +``` +npm config set sign-git-tag true +``` + +Release it to npm using the [npm version command](https://docs.npmjs.com/cli/version). Like: + +``` +npm version patch +``` + +This will: + +* Bump a patch version +* Commit the change +* Generate the tag +* Push the commit and the tag to the repository +* Publish the package in https://www.npmjs.com diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..e0e44e67 --- /dev/null +++ b/Rakefile @@ -0,0 +1,47 @@ +desc %(Starts the test server and opens it in a web browser) +multitask :default => ['test:server', 'test:open'] + +PORT = 4567 + +namespace :test do + desc %(Starts the test server) + task :server do + system 'bundle exec ruby test/server.rb' + end + + desc %(Starts the test server which reloads everything on each refresh) + task :reloadable do + exec "bundle exec shotgun test/config.ru -p #{PORT} --server thin" + end + + task :open do + url = "http://localhost:#{PORT}" + puts "Opening test app at #{url} ..." + sleep 3 + system( *browse_cmd(url) ) + end +end + +# Returns an array e.g.: ['open', 'http://example.com'] +def browse_cmd(url) + require 'rbconfig' + browser = ENV['BROWSER'] || + (RbConfig::CONFIG['host_os'].include?('darwin') && 'open') || + (RbConfig::CONFIG['host_os'] =~ /msdos|mswin|djgpp|mingw|windows/ && 'start') || + %w[xdg-open x-www-browser firefox opera mozilla netscape].find { |comm| which comm } + + abort('ERROR: no web browser detected') unless browser + Array(browser) << url +end + +# which('ruby') #=> /usr/bin/ruby +def which cmd + exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] + ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| + exts.each { |ext| + exe = "#{path}/#{cmd}#{ext}" + return exe if File.executable? exe + } + end + return nil +end diff --git a/bower.json b/bower.json new file mode 100644 index 00000000..14f93cf3 --- /dev/null +++ b/bower.json @@ -0,0 +1,19 @@ +{ + "name": "jquery-ujs", + "homepage": "https://github.com/rails/jquery-ujs", + "authors": ["Stephen St. Martin", "Steve Schwartz"], + "description": "Ruby on Rails unobtrusive scripting adapter for jQuery", + "main": "src/rails.js", + "license": "MIT", + "dependencies": { + "jquery": ">1.8.*" + }, + "ignore": [ + "**/.*", + "Gemfile*", + "Rakefile", + "bower_components", + "script", + "test" + ] +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..9299d5f5 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "jquery-ujs", + "version": "1.2.3", + "description": "Unobtrusive scripting adapter for jQuery", + "main": "src/rails.js", + "scripts": { + "test": "echo \"See the wiki: https://github.com/rails/jquery-ujs/wiki/Running-Tests-and-Contributing\" && exit 1", + "postversion": "git push && git push --tags && npm publish" + }, + "repository": { + "type": "git", + "url": "https://github.com/rails/jquery-ujs.git" + }, + "author": [ + "Stephen St. Martin", + "Steve Schwartz" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/rails/jquery-ujs/issues" + }, + "homepage": "https://github.com/rails/jquery-ujs#readme", + "peerDependencies": { + "jquery": ">=1.8.0" + } +} diff --git a/script/cibuild b/script/cibuild new file mode 100755 index 00000000..45c97099 --- /dev/null +++ b/script/cibuild @@ -0,0 +1,40 @@ +#!/bin/bash + +port=4567 + +start_server() { + mkdir -p log + bundle exec ruby test/server.rb > log/test.log 2>&1 & +} + +run_tests() { + jshint src/*.js && phantomjs script/runner.js http://localhost:$port/ +} + +server_started() { + lsof -i :${1?} >/dev/null +} + +timestamp() { + date +%s +} + +wait_for_server() { + timeout=$(( `timestamp` + $1 )) + while true; do + if server_started "$2"; then + break + elif [ `timestamp` -gt "$timeout" ]; then + echo "timed out after $1 seconds" >&2 + exit 1 + fi + done +} + +start_server +server_pid=$! +wait_for_server 5 $port +run_tests +result=$? +kill $server_pid +exit $result diff --git a/script/runner.js b/script/runner.js new file mode 100644 index 00000000..5d9f96c5 --- /dev/null +++ b/script/runner.js @@ -0,0 +1,148 @@ +/* + * PhantomJS Runner QUnit Plugin 1.2.0 + * + * PhantomJS binaries: http://phantomjs.org/download.html + * Requires PhantomJS 1.6+ (1.7+ recommended) + * + * Run with: + * phantomjs runner.js [url-of-your-qunit-testsuite] + * + * e.g. + * phantomjs runner.js http://localhost/qunit/test/index.html + */ + +/*global phantom:false, require:false, console:false, window:false, QUnit:false */ + +(function() { + 'use strict'; + + var url, page, timeout, + args = require('system').args; + + // arg[0]: scriptName, args[1...]: arguments + if (args.length < 2 || args.length > 3) { + console.error('Usage:\n phantomjs runner.js [url-of-your-qunit-testsuite] [timeout-in-seconds]'); + phantom.exit(1); + } + + url = args[1]; + page = require('webpage').create(); + if (args[2] !== undefined) { + timeout = parseInt(args[2], 10); + } + + // Route `console.log()` calls from within the Page context to the main Phantom context (i.e. current `this`) + page.onConsoleMessage = function(msg) { + console.log(msg); + }; + + page.onInitialized = function() { + page.evaluate(addLogging); + }; + + page.onCallback = function(message) { + var result, + failed; + + if (message) { + if (message.name === 'QUnit.done') { + result = message.data; + failed = !result || !result.total || result.failed; + + if (!result.total) { + console.error('No tests were executed. Are you loading tests asynchronously?'); + } + + phantom.exit(failed ? 1 : 0); + } + } + }; + + page.open(url, function(status) { + if (status !== 'success') { + console.error('Unable to access network: ' + status); + phantom.exit(1); + } else { + // Cannot do this verification with the 'DOMContentLoaded' handler because it + // will be too late to attach it if a page does not have any script tags. + var qunitMissing = page.evaluate(function() { return (typeof QUnit === 'undefined' || !QUnit); }); + if (qunitMissing) { + console.error('The `QUnit` object is not present on this page.'); + phantom.exit(1); + } + + // Set a timeout on the test running, otherwise tests with async problems will hang forever + if (typeof timeout === 'number') { + setTimeout(function() { + console.error('The specified timeout of ' + timeout + ' seconds has expired. Aborting...'); + phantom.exit(1); + }, timeout * 1000); + } + + // Do nothing... the callback mechanism will handle everything! + } + }); + + function addLogging() { + window.document.addEventListener('DOMContentLoaded', function() { + var currentTestAssertions = []; + + QUnit.log(function(details) { + var response; + + // Ignore passing assertions + if (details.result) { + return; + } + + response = details.message || ''; + + if (typeof details.expected !== 'undefined') { + if (response) { + response += ', '; + } + + response += 'expected: ' + details.expected + ', but was: ' + details.actual; + } + + if (details.source) { + response += "\n" + details.source; + } + + currentTestAssertions.push('Failed assertion: ' + response); + }); + + QUnit.testDone(function(result) { + var i, + len, + name = ''; + + if (result.module) { + name += result.module + ': '; + } + name += result.name; + + if (result.failed) { + console.log('\n' + 'Test failed: ' + name); + + for (i = 0, len = currentTestAssertions.length; i < len; i++) { + console.log(' ' + currentTestAssertions[i]); + } + } + + currentTestAssertions.length = 0; + }); + + QUnit.done(function(result) { + console.log('\n' + 'Took ' + result.runtime + 'ms to run ' + result.total + ' tests. ' + result.passed + ' passed, ' + result.failed + ' failed.'); + + if (typeof window.callPhantom === 'function') { + window.callPhantom({ + 'name': 'QUnit.done', + 'data': result + }); + } + }); + }, false); + } +})(); diff --git a/src/rails.js b/src/rails.js index 223fe7ea..0e27110d 100644 --- a/src/rails.js +++ b/src/rails.js @@ -1,156 +1,565 @@ -/* - * jquery-ujs - * - * http://github.com/rails/jquery-ujs/blob/master/src/rails.js - * - * This rails.js file supports jQuery 1.4.3 and 1.4.4 . - * - */ - -jQuery(function ($) { - var csrf_token = $('meta[name=csrf-token]').attr('content'), - csrf_param = $('meta[name=csrf-param]').attr('content'); - - $.fn.extend({ - /** - * Triggers a custom event on an element and returns the event result - * this is used to get around not being able to ensure callbacks are placed - * at the end of the chain. - * - * TODO: deprecate with jQuery 1.4.2 release, in favor of subscribing to our - * own events and placing ourselves at the end of the chain. - */ - triggerAndReturn: function (name, data) { - var event = new $.Event(name); - this.trigger(event, data); - - return event.result !== false; - }, - - /** - * Handles execution of remote calls. Provides following callbacks: - * - * - ajax:before - is execute before the whole thing begings - * - ajax:loading - is executed before firing ajax call - * - ajax:success - is executed when status is success - * - ajax:complete - is execute when status is complete - * - ajax:failure - is execute in case of error - * - ajax:after - is execute every single time at the end of ajax call - */ - callRemote: function () { - var el = this, - method = el.attr('method') || el.attr('data-method') || 'GET', - url = el.attr('action') || el.attr('href'), - dataType = el.attr('data-type') || 'script'; - - if (url === undefined) { - throw "No URL specified for remote call (action or href must be present)."; - } else { - if (el.triggerAndReturn('ajax:before')) { - var data = el.is('form') ? el.serializeArray() : []; - $.ajax({ - url: url, - data: data, - dataType: dataType, - type: method.toUpperCase(), - beforeSend: function (xhr) { - el.trigger('ajax:loading', xhr); - }, - success: function (data, status, xhr) { - el.trigger('ajax:success', [data, status, xhr]); - }, - complete: function (xhr) { - el.trigger('ajax:complete', xhr); - }, - error: function (xhr, status, error) { - el.trigger('ajax:failure', [xhr, status, error]); - } - }); - } - - el.trigger('ajax:after'); - } - } - }); - - /** - * confirmation handler - */ - - $('body').delegate('a[data-confirm], button[data-confirm], input[data-confirm]', 'click', function () { - var el = $(this); - if (el.triggerAndReturn('confirm')) { - if (!confirm(el.attr('data-confirm'))) { - return false; - } - } - }); - - - - /** - * remote handlers - */ - $('form[data-remote]').live('submit', function (e) { - $(this).callRemote(); - e.preventDefault(); - }); - - $('a[data-remote],input[data-remote]').live('click', function (e) { - $(this).callRemote(); - e.preventDefault(); - }); - - $('a[data-method]:not([data-remote])').live('click', function (e){ - var link = $(this), - href = link.attr('href'), - method = link.attr('data-method'), - form = $('
'), - metadata_input = ''; - - if (csrf_param != null && csrf_token != null) { - metadata_input += ''; - } - - form.hide() - .append(metadata_input) - .appendTo('body'); - - e.preventDefault(); - form.submit(); - }); - - /** - * disable-with handlers - */ - var disable_with_input_selector = 'input[data-disable-with]', - disable_with_form_remote_selector = 'form[data-remote]:has(' + disable_with_input_selector + ')', - disable_with_form_not_remote_selector = 'form:not([data-remote]):has(' + disable_with_input_selector + ')'; - - var disable_with_input_function = function () { - $(this).find(disable_with_input_selector).each(function () { - var input = $(this); - input.data('enable-with', input.val()) - .attr('value', input.attr('data-disable-with')) - .attr('disabled', 'disabled'); - }); - }; - - $(disable_with_form_remote_selector).live('ajax:before', disable_with_input_function); - $(disable_with_form_not_remote_selector).live('submit', disable_with_input_function); - - $(disable_with_form_remote_selector).live('ajax:complete', function () { - $(this).find(disable_with_input_selector).each(function () { - var input = $(this); - input.removeAttr('disabled') - .val(input.data('enable-with')); - }); - }); - - var jqueryVersion = $().jquery; - - if ( (jqueryVersion === '1.4') || (jqueryVersion === '1.4.1') || (jqueryVersion === '1.4.2') ){ - alert('This rails.js does not support the jQuery version you are using. Please read documentation.') - } - -}); +/* jshint node: true */ + +/** + * Unobtrusive scripting adapter for jQuery + * https://github.com/rails/jquery-ujs + * + * Requires jQuery 1.8.0 or later. + * + * Released under the MIT license + * + */ + +(function() { + 'use strict'; + + var jqueryUjsInit = function($, undefined) { + + // Cut down on the number of issues from people inadvertently including jquery_ujs twice + // by detecting and raising an error when it happens. + if ( $.rails !== undefined ) { + $.error('jquery-ujs has already been loaded!'); + } + + // Shorthand to make it a little easier to call public rails functions from within rails.js + var rails; + var $document = $(document); + + $.rails = rails = { + // Link elements bound by jquery-ujs + linkClickSelector: 'a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]', + + // Button elements bound by jquery-ujs + buttonClickSelector: 'button[data-remote]:not([form]):not(form button), button[data-confirm]:not([form]):not(form button)', + + // Select elements bound by jquery-ujs + inputChangeSelector: 'select[data-remote], input[data-remote], textarea[data-remote]', + + // Form elements bound by jquery-ujs + formSubmitSelector: 'form:not([data-turbo=true])', + + // Form input elements bound by jquery-ujs + formInputClickSelector: 'form:not([data-turbo=true]) input[type=submit], form:not([data-turbo=true]) input[type=image], form:not([data-turbo=true]) button[type=submit], form:not([data-turbo=true]) button:not([type]), input[type=submit][form], input[type=image][form], button[type=submit][form], button[form]:not([type])', + + // Form input elements disabled during form submission + disableSelector: 'input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled', + + // Form input elements re-enabled after form submission + enableSelector: 'input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled', + + // Form required input elements + requiredInputSelector: 'input[name][required]:not([disabled]), textarea[name][required]:not([disabled])', + + // Form file input elements + fileInputSelector: 'input[name][type=file]:not([disabled])', + + // Link onClick disable selector with possible reenable after remote submission + linkDisableSelector: 'a[data-disable-with], a[data-disable]', + + // Button onClick disable selector with possible reenable after remote submission + buttonDisableSelector: 'button[data-remote][data-disable-with], button[data-remote][data-disable]', + + // Up-to-date Cross-Site Request Forgery token + csrfToken: function() { + return $('meta[name=csrf-token]').attr('content'); + }, + + // URL param that must contain the CSRF token + csrfParam: function() { + return $('meta[name=csrf-param]').attr('content'); + }, + + // Make sure that every Ajax request sends the CSRF token + CSRFProtection: function(xhr) { + var token = rails.csrfToken(); + if (token) xhr.setRequestHeader('X-CSRF-Token', token); + }, + + // Make sure that all forms have actual up-to-date tokens (cached forms contain old ones) + refreshCSRFTokens: function(){ + $('form input[name="' + rails.csrfParam() + '"]').val(rails.csrfToken()); + }, + + // Triggers an event on an element and returns false if the event result is false + fire: function(obj, name, data) { + var event = $.Event(name); + obj.trigger(event, data); + return event.result !== false; + }, + + // Default confirm dialog, may be overridden with custom confirm dialog in $.rails.confirm + confirm: function(message) { + return confirm(message); + }, + + // Default ajax function, may be overridden with custom function in $.rails.ajax + ajax: function(options) { + return $.ajax(options); + }, + + // Default way to get an element's href. May be overridden at $.rails.href. + href: function(element) { + return element[0].href; + }, + + // Checks "data-remote" if true to handle the request through a XHR request. + isRemote: function(element) { + return element.data('remote') !== undefined && element.data('remote') !== false; + }, + + // Submits "remote" forms and links with ajax + handleRemote: function(element) { + var method, url, data, withCredentials, dataType, options; + + if (rails.fire(element, 'ajax:before')) { + withCredentials = element.data('with-credentials') || null; + dataType = element.data('type') || ($.ajaxSettings && $.ajaxSettings.dataType); + + if (element.is('form')) { + method = element.data('ujs:submit-button-formmethod') || element.attr('method'); + url = element.data('ujs:submit-button-formaction') || element.attr('action'); + data = $(element[0]).serializeArray(); + // memoized value from clicked submit button + var button = element.data('ujs:submit-button'); + if (button) { + data.push(button); + element.data('ujs:submit-button', null); + } + element.data('ujs:submit-button-formmethod', null); + element.data('ujs:submit-button-formaction', null); + } else if (element.is(rails.inputChangeSelector)) { + method = element.data('method'); + url = element.data('url'); + data = element.serialize(); + if (element.data('params')) data = data + '&' + element.data('params'); + } else if (element.is(rails.buttonClickSelector)) { + method = element.data('method') || 'get'; + url = element.data('url'); + data = element.serialize(); + if (element.data('params')) data = data + '&' + element.data('params'); + } else { + method = element.data('method'); + url = rails.href(element); + data = element.data('params') || null; + } + + options = { + type: method || 'GET', data: data, dataType: dataType, + // stopping the "ajax:beforeSend" event will cancel the ajax request + beforeSend: function(xhr, settings) { + if (settings.dataType === undefined) { + xhr.setRequestHeader('accept', '*/*;q=0.5, ' + settings.accepts.script); + } + if (rails.fire(element, 'ajax:beforeSend', [xhr, settings])) { + element.trigger('ajax:send', xhr); + } else { + return false; + } + }, + success: function(data, status, xhr) { + element.trigger('ajax:success', [data, status, xhr]); + }, + complete: function(xhr, status) { + element.trigger('ajax:complete', [xhr, status]); + }, + error: function(xhr, status, error) { + element.trigger('ajax:error', [xhr, status, error]); + }, + crossDomain: rails.isCrossDomain(url) + }; + + // There is no withCredentials for IE6-8 when + // "Enable native XMLHTTP support" is disabled + if (withCredentials) { + options.xhrFields = { + withCredentials: withCredentials + }; + } + + // Only pass url to `ajax` options if not blank + if (url) { options.url = url; } + + return rails.ajax(options); + } else { + return false; + } + }, + + // Determines if the request is a cross domain request. + isCrossDomain: function(url) { + var originAnchor = document.createElement('a'); + originAnchor.href = location.href; + var urlAnchor = document.createElement('a'); + + try { + urlAnchor.href = url; + // This is a workaround to a IE bug. + urlAnchor.href = urlAnchor.href; + + // If URL protocol is false or is a string containing a single colon + // *and* host are false, assume it is not a cross-domain request + // (should only be the case for IE7 and IE compatibility mode). + // Otherwise, evaluate protocol and host of the URL against the origin + // protocol and host. + return !(((!urlAnchor.protocol || urlAnchor.protocol === ':') && !urlAnchor.host) || + (originAnchor.protocol + '//' + originAnchor.host === + urlAnchor.protocol + '//' + urlAnchor.host)); + } catch (e) { + // If there is an error parsing the URL, assume it is crossDomain. + return true; + } + }, + + // Handles "data-method" on links such as: + // Delete + handleMethod: function(link) { + var href = rails.href(link), + method = link.data('method'), + target = link.attr('target'), + csrfToken = rails.csrfToken(), + csrfParam = rails.csrfParam(), + form = $('
'), + metadataInput = ''; + + if (csrfParam !== undefined && csrfToken !== undefined && !rails.isCrossDomain(href)) { + metadataInput += ''; + } + + if (target) { form.attr('target', target); } + + form.hide().append(metadataInput).appendTo('body'); + form.submit(); + }, + + // Helper function that returns form elements that match the specified CSS selector + // If form is actually a "form" element this will return associated elements outside the from that have + // the html form attribute set + formElements: function(form, selector) { + return form.is('form') ? $(form[0].elements).filter(selector) : form.find(selector); + }, + + /* Disables form elements: + - Caches element value in 'ujs:enable-with' data store + - Replaces element text with value of 'data-disable-with' attribute + - Sets disabled property to true + */ + disableFormElements: function(form) { + rails.formElements(form, rails.disableSelector).each(function() { + rails.disableFormElement($(this)); + }); + }, + + disableFormElement: function(element) { + var method, replacement; + + method = element.is('button') ? 'html' : 'val'; + replacement = element.data('disable-with'); + + if (replacement !== undefined) { + element.data('ujs:enable-with', element[method]()); + element[method](replacement); + } + + element.prop('disabled', true); + element.data('ujs:disabled', true); + }, + + /* Re-enables disabled form elements: + - Replaces element text with cached value from 'ujs:enable-with' data store (created in `disableFormElements`) + - Sets disabled property to false + */ + enableFormElements: function(form) { + rails.formElements(form, rails.enableSelector).each(function() { + rails.enableFormElement($(this)); + }); + }, + + enableFormElement: function(element) { + var method = element.is('button') ? 'html' : 'val'; + if (element.data('ujs:enable-with') !== undefined) { + element[method](element.data('ujs:enable-with')); + element.removeData('ujs:enable-with'); // clean up cache + } + element.prop('disabled', false); + element.removeData('ujs:disabled'); + }, + + /* For 'data-confirm' attribute: + - Fires `confirm` event + - Shows the confirmation dialog + - Fires the `confirm:complete` event + + Returns `true` if no function stops the chain and user chose yes; `false` otherwise. + Attaching a handler to the element's `confirm` event that returns a `falsy` value cancels the confirmation dialog. + Attaching a handler to the element's `confirm:complete` event that returns a `falsy` value makes this function + return false. The `confirm:complete` event is fired whether or not the user answered true or false to the dialog. + */ + allowAction: function(element) { + var message = element.data('confirm'), + answer = false, callback; + if (!message) { return true; } + + if (rails.fire(element, 'confirm')) { + try { + answer = rails.confirm(message); + } catch (e) { + (console.error || console.log).call(console, e.stack || e); + } + callback = rails.fire(element, 'confirm:complete', [answer]); + } + return answer && callback; + }, + + // Helper function which checks for blank inputs in a form that match the specified CSS selector + blankInputs: function(form, specifiedSelector, nonBlank) { + var foundInputs = $(), + input, + valueToCheck, + radiosForNameWithNoneSelected, + radioName, + selector = specifiedSelector || 'input,textarea', + requiredInputs = form.find(selector), + checkedRadioButtonNames = {}; + + requiredInputs.each(function() { + input = $(this); + if (input.is('input[type=radio]')) { + + // Don't count unchecked required radio as blank if other radio with same name is checked, + // regardless of whether same-name radio input has required attribute or not. The spec + // states https://www.w3.org/TR/html5/forms.html#the-required-attribute + radioName = input.attr('name'); + + // Skip if we've already seen the radio with this name. + if (!checkedRadioButtonNames[radioName]) { + + // If none checked + if (form.find('input[type=radio]:checked[name="' + radioName + '"]').length === 0) { + radiosForNameWithNoneSelected = form.find( + 'input[type=radio][name="' + radioName + '"]'); + foundInputs = foundInputs.add(radiosForNameWithNoneSelected); + } + + // We only need to check each name once. + checkedRadioButtonNames[radioName] = radioName; + } + } else { + valueToCheck = input.is('input[type=checkbox],input[type=radio]') ? input.is(':checked') : !!input.val(); + if (valueToCheck === nonBlank) { + foundInputs = foundInputs.add(input); + } + } + }); + return foundInputs.length ? foundInputs : false; + }, + + // Helper function which checks for non-blank inputs in a form that match the specified CSS selector + nonBlankInputs: function(form, specifiedSelector) { + return rails.blankInputs(form, specifiedSelector, true); // true specifies nonBlank + }, + + // Helper function, needed to provide consistent behavior in IE + stopEverything: function(e) { + $(e.target).trigger('ujs:everythingStopped'); + e.stopImmediatePropagation(); + return false; + }, + + // Replace element's html with the 'data-disable-with' after storing original html + // and prevent clicking on it + disableElement: function(element) { + var replacement = element.data('disable-with'); + + if (replacement !== undefined) { + element.data('ujs:enable-with', element.html()); // store enabled state + element.html(replacement); + } + + element.on('click.railsDisable', function(e) { // prevent further clicking + return rails.stopEverything(e); + }); + element.data('ujs:disabled', true); + }, + + // Restore element to its original state which was disabled by 'disableElement' above + enableElement: function(element) { + if (element.data('ujs:enable-with') !== undefined) { + element.html(element.data('ujs:enable-with')); // set to old enabled state + element.removeData('ujs:enable-with'); // clean up cache + } + element.off('click.railsDisable'); // enable element + element.removeData('ujs:disabled'); + } + }; + + if (rails.fire($document, 'rails:attachBindings')) { + + $.ajaxPrefilter(function(options, originalOptions, xhr){ if ( !options.crossDomain ) { rails.CSRFProtection(xhr); }}); + + // This event works the same as the load event, except that it fires every + // time the page is loaded. + // + // See https://github.com/rails/jquery-ujs/issues/357 + // See https://developer.mozilla.org/en-US/docs/Using_Firefox_1.5_caching + $(window).on('pageshow.rails', function () { + $($.rails.enableSelector).each(function () { + var element = $(this); + + if (element.data('ujs:disabled')) { + $.rails.enableFormElement(element); + } + }); + + $($.rails.linkDisableSelector).each(function () { + var element = $(this); + + if (element.data('ujs:disabled')) { + $.rails.enableElement(element); + } + }); + }); + + $document.on('ajax:complete', rails.linkDisableSelector, function() { + rails.enableElement($(this)); + }); + + $document.on('ajax:complete', rails.buttonDisableSelector, function() { + rails.enableFormElement($(this)); + }); + + $document.on('click.rails', rails.linkClickSelector, function(e) { + var link = $(this), method = link.data('method'), data = link.data('params'), metaClick = e.metaKey || e.ctrlKey; + if (!rails.allowAction(link)) return rails.stopEverything(e); + + if (!metaClick && link.is(rails.linkDisableSelector)) rails.disableElement(link); + + if (rails.isRemote(link)) { + if (metaClick && (!method || method === 'GET') && !data) { return true; } + + var handleRemote = rails.handleRemote(link); + // Response from rails.handleRemote() will either be false or a deferred object promise. + if (handleRemote === false) { + rails.enableElement(link); + } else { + handleRemote.fail( function() { rails.enableElement(link); } ); + } + return false; + + } else if (method) { + rails.handleMethod(link); + return false; + } + }); + + $document.on('click.rails', rails.buttonClickSelector, function(e) { + var button = $(this); + + if (!rails.allowAction(button) || !rails.isRemote(button)) return rails.stopEverything(e); + + if (button.is(rails.buttonDisableSelector)) rails.disableFormElement(button); + + var handleRemote = rails.handleRemote(button); + // Response from rails.handleRemote() will either be false or a deferred object promise. + if (handleRemote === false) { + rails.enableFormElement(button); + } else { + handleRemote.fail( function() { rails.enableFormElement(button); } ); + } + return false; + }); + + $document.on('change.rails', rails.inputChangeSelector, function(e) { + var link = $(this); + if (!rails.allowAction(link) || !rails.isRemote(link)) return rails.stopEverything(e); + + rails.handleRemote(link); + return false; + }); + + $document.on('submit.rails', rails.formSubmitSelector, function(e) { + var form = $(this), + remote = rails.isRemote(form), + blankRequiredInputs, + nonBlankFileInputs; + + if (!rails.allowAction(form)) return rails.stopEverything(e); + + // Skip other logic when required values are missing or file upload is present + if (form.attr('novalidate') === undefined) { + if (form.data('ujs:formnovalidate-button') === undefined) { + blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector, false); + if (blankRequiredInputs && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) { + return rails.stopEverything(e); + } + } else { + // Clear the formnovalidate in case the next button click is not on a formnovalidate button + // Not strictly necessary to do here, since it is also reset on each button click, but just to be certain + form.data('ujs:formnovalidate-button', undefined); + } + } + + if (remote) { + nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector); + if (nonBlankFileInputs) { + // Slight timeout so that the submit button gets properly serialized + // (make it easy for event handler to serialize form without disabled values) + setTimeout(function(){ rails.disableFormElements(form); }, 13); + var aborted = rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]); + + // Re-enable form elements if event bindings return false (canceling normal form submission) + if (!aborted) { setTimeout(function(){ rails.enableFormElements(form); }, 13); } + + return aborted; + } + + rails.handleRemote(form); + return false; + + } else { + // Slight timeout so that the submit button gets properly serialized + setTimeout(function(){ rails.disableFormElements(form); }, 13); + } + }); + + $document.on('click.rails', rails.formInputClickSelector, function(event) { + var button = $(this); + + if (!rails.allowAction(button)) return rails.stopEverything(event); + + // Register the pressed submit button + var name = button.attr('name'), + data = name ? {name:name, value:button.val()} : null; + + var form = button.closest('form'); + if (form.length === 0) { + form = $('#' + button.attr('form')); + } + form.data('ujs:submit-button', data); + + // Save attributes from button + form.data('ujs:formnovalidate-button', button.attr('formnovalidate')); + form.data('ujs:submit-button-formaction', button.attr('formaction')); + form.data('ujs:submit-button-formmethod', button.attr('formmethod')); + }); + + $document.on('ajax:send.rails', rails.formSubmitSelector, function(event) { + if (this === event.target) rails.disableFormElements($(this)); + }); + + $document.on('ajax:complete.rails', rails.formSubmitSelector, function(event) { + if (this === event.target) rails.enableFormElements($(this)); + }); + + $(function(){ + rails.refreshCSRFTokens(); + }); + } + + }; + + if (window.jQuery) { + jqueryUjsInit(jQuery); + } else if (typeof exports === 'object' && typeof module === 'object') { + module.exports = jqueryUjsInit; + } +})(); diff --git a/test/config.ru b/test/config.ru new file mode 100644 index 00000000..44fb4f03 --- /dev/null +++ b/test/config.ru @@ -0,0 +1,3 @@ +$LOAD_PATH.unshift File.expand_path('..', __FILE__) +require 'server' +run Sinatra::Application diff --git a/test/public/qunit.css b/test/public/qunit.css deleted file mode 100644 index 9eafd240..00000000 --- a/test/public/qunit.css +++ /dev/null @@ -1,118 +0,0 @@ -ol#qunit-tests { - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - margin:0; - padding:0; - list-style-position:inside; - - font-size: smaller; -} -ol#qunit-tests li{ - padding:0.4em 0.5em 0.4em 2.5em; - border-bottom:1px solid #fff; - font-size:small; - list-style-position:inside; -} -ol#qunit-tests li ol{ - box-shadow: inset 0px 2px 13px #999; - -moz-box-shadow: inset 0px 2px 13px #999; - -webkit-box-shadow: inset 0px 2px 13px #999; - margin-top:0.5em; - margin-left:0; - padding:0.5em; - background-color:#fff; - border-radius:15px; - -moz-border-radius: 15px; - -webkit-border-radius: 15px; -} -ol#qunit-tests li li{ - border-bottom:none; - margin:0.5em; - background-color:#fff; - list-style-position: inside; - padding:0.4em 0.5em 0.4em 0.5em; -} - -ol#qunit-tests li li.pass{ - border-left:26px solid #C6E746; - background-color:#fff; - color:#5E740B; - } -ol#qunit-tests li li.fail{ - border-left:26px solid #EE5757; - background-color:#fff; - color:#710909; -} -ol#qunit-tests li.pass{ - background-color:#D2E0E6; - color:#528CE0; -} -ol#qunit-tests li.fail{ - background-color:#EE5757; - color:#000; -} -ol#qunit-tests li strong { - cursor:pointer; -} -h1#qunit-header{ - background-color:#0d3349; - margin:0; - padding:0.5em 0 0.5em 1em; - color:#fff; - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - border-top-right-radius:15px; - border-top-left-radius:15px; - -moz-border-radius-topright:15px; - -moz-border-radius-topleft:15px; - -webkit-border-top-right-radius:15px; - -webkit-border-top-left-radius:15px; - text-shadow: rgba(0, 0, 0, 0.5) 4px 4px 1px; -} -h2#qunit-banner{ - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - height:5px; - margin:0; - padding:0; -} -h2#qunit-banner.qunit-pass{ - background-color:#C6E746; -} -h2#qunit-banner.qunit-fail, #qunit-testrunner-toolbar { - background-color:#EE5757; -} -#qunit-testrunner-toolbar { - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - padding:0; - /*width:80%;*/ - padding:0em 0 0.5em 2em; - font-size: small; -} -h2#qunit-userAgent { - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - background-color:#2b81af; - margin:0; - padding:0; - color:#fff; - font-size: small; - padding:0.5em 0 0.5em 2.5em; - text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; -} -p#qunit-testresult{ - font-family:"Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial; - margin:0; - font-size: small; - color:#2b81af; - border-bottom-right-radius:15px; - border-bottom-left-radius:15px; - -moz-border-radius-bottomright:15px; - -moz-border-radius-bottomleft:15px; - -webkit-border-bottom-right-radius:15px; - -webkit-border-bottom-left-radius:15px; - background-color:#D2E0E6; - padding:0.5em 0.5em 0.5em 2.5em; -} -strong b.fail{ - color:#710909; - } -strong b.pass{ - color:#5E740B; - } diff --git a/test/public/test/call-remote-callbacks.js b/test/public/test/call-remote-callbacks.js index aecd37dd..7dd1d6ac 100644 --- a/test/public/test/call-remote-callbacks.js +++ b/test/public/test/call-remote-callbacks.js @@ -1,79 +1,465 @@ -var App = App || {}; +(function(){ -App.build_form = function(opt) { - var defaults = { - 'data-remote': 'true' - }; +module('call-remote-callbacks', { + setup: function() { + $('#qunit-fixture').append($('
', { + action: '/echo', method: 'get', 'data-remote': 'true' + })); + }, + teardown: function() { + $(document).off('ajax:beforeSend', 'form[data-remote]'); + $(document).off('ajax:before', 'form[data-remote]'); + $(document).off('ajax:send', 'form[data-remote]'); + $(document).off('ajax:complete', 'form[data-remote]'); + $(document).off('ajax:success', 'form[data-remote]'); + $(document).off('ajaxStop'); + $(document).off('iframe:loading'); + } +}); + +function start_after_submit(form) { + form.on('ajax:complete', function() { + ok(true, 'ajax:complete'); + start(); + }); +} + +function submit(fn) { + var form = $('form'); + start_after_submit(form); - var options = $.extend(defaults, opt); + if (fn) fn(form); + form.trigger('submit'); +} - $('#fixtures').append($('', options)); +function submit_with_button(submit_button) { + var form = $('form'); + start_after_submit(form); - $('form').append($('', { - id: 'user_name', - type: 'text', - size: '30', - 'name': 'user_name', - 'value': 'john' - })); -}; + submit_button.trigger('click'); +} -module('call-remote', { +asyncTest('modifying form fields with "ajax:before" sends modified data in request', 4, function(){ + $('form[data-remote]') + .append($('')) + .append($('')) + .on('ajax:before', function() { + var form = $(this); + form + .append($('',{name: 'other_user_name',value: 'jonathan'})) + .find('input[name="removed_user_name"]').remove(); + form + .find('input[name="user_name"]').val('steve'); + }); + + submit(function(form) { + form.on('ajax:success', function(e, data, status, xhr) { + equal(data.params.user_name, 'steve', 'modified field value should have been submitted'); + equal(data.params.other_user_name, 'jonathan', 'added field value should have been submitted'); + equal(data.params.removed_user_name, undefined, 'removed field value should be undefined'); + }); + }); +}); - teardown: App.teardown, +asyncTest('modifying data("type") with "ajax:before" requests new dataType in request', 2, function(){ + $('form[data-remote]').data('type','html') + .on('ajax:before', function() { + var form = $(this); + form.data('type','xml'); + }); - setup: function() { - App.build_form({ - 'action': App.url('show') - }); - } + submit(function(form) { + form.on('ajax:beforeSend', function(e, xhr, settings) { + equal(settings.dataType, 'xml', 'modified dataType should have been requested'); + }); + }); }); -test('if ajax:before callback returns false then do not proceed', function() { - expect(0); - stop(); +asyncTest('setting data("with-credentials",true) with "ajax:before" uses new setting in request', 2, function(){ + $('form[data-remote]').data('with-credentials',false) + .on('ajax:before', function() { + var form = $(this); + form.data('with-credentials',true); + }); - $('form') - .bind('ajax:before', function() { return false; }) - .bind('ajax:loading', function(){ - ok(false, 'ajax call should not have been made since ajax:before callback returns false'); + submit(function(form) { + form.on('ajax:beforeSend', function(e, xhr, settings) { + equal(settings.xhrFields && settings.xhrFields.withCredentials, true, 'setting modified in ajax:before should have forced withCredentials request'); }); + }); +}); - $('form[data-remote]').trigger('submit'); +asyncTest('stopping the "ajax:beforeSend" event aborts the request', 1, function() { + submit(function(form) { + form.on('ajax:beforeSend', function() { + ok(true, 'aborting request in ajax:beforeSend'); + return false; + }); + form.off('ajax:send').on('ajax:send', function() { + ok(false, 'ajax:send should not run'); + }); + form.off('ajax:complete').on('ajax:complete', function() { + ok(false, 'ajax:complete should not run'); + }); + form.on('ajax:error', function(e, xhr, status, error) { + ok(false, 'ajax:error should not run'); + }); + $(document).on('ajaxStop', function() { + start(); + }); + }); +}); + +asyncTest('blank required form input field should abort request and trigger "ajax:aborted:required" event', 5, function() { + $(document).on('iframe:loading', function() { + ok(false, 'form should not get submitted'); + }); + + var form = $('form[data-remote]') + .append($('')) + .append($('')) + .on('ajax:beforeSend', function() { + ok(false, 'ajax:beforeSend should not run'); + }) + .on('ajax:aborted:required', function(e,data){ + ok(data.length == 2, 'ajax:aborted:required event is passed all blank required inputs (jQuery objects)'); + ok(data.first().is('input[name="user_name"]') , 'ajax:aborted:required adds blank required input to data'); + ok(data.last().is('textarea[name="user_bio"]'), 'ajax:aborted:required adds blank required textarea to data'); + ok(true, 'ajax:aborted:required should run'); + }) + .trigger('submit'); + + setTimeout(function() { + form.find('input[required],textarea[required]').val('Tyler'); + form.off('ajax:beforeSend'); + submit(); + }, 13); +}); + +asyncTest('blank required form input for non-remote form should abort normal submission', 1, function() { + var form = $('form[data-remote]') + .append($('')) + .removeAttr('data-remote') + .on('ujs:everythingStopped', function() { + ok(true, 'ujs:everythingStopped should run'); + }) + .trigger('submit'); + + setTimeout(function() { + start(); + }, 13); +}); + +asyncTest('form should be submitted with blank required fields if handler is bound to "ajax:aborted:required" event that returns false', 1, function(){ + var form = $('form[data-remote]') + .append($('')) + .on('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run'); + }) + .on('ajax:aborted:required', function() { + return false; + }) + .trigger('submit'); + + setTimeout(function() { + start(); + }, 13); +}); + +asyncTest('disabled fields should not be included in blank required check', 2, function() { + var form = $('form[data-remote]') + .append($('')) + .append($('')) + .on('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run'); + }) + .on('ajax:aborted:required', function() { + ok(false, 'ajax:aborted:required should not run'); + }); + + submit(); +}); + +asyncTest('form should be submitted with blank required fields if it has the "novalidate" attribute', 2, function(){ + var form = $('form[data-remote]') + .append($('')) + .attr("novalidate", "novalidate") + .on('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run'); + }) + .on('ajax:aborted:required', function() { + ok(false, 'ajax:aborted:required should not run'); + }); + + submit(); +}); + +asyncTest('form should be submitted with blank required fields if the button has the "formnovalidate" attribute', 2, function(){ + var submit_button = $(''); + var form = $('form[data-remote]') + .append($('')) + .append(submit_button) + .on('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run'); + }) + .on('ajax:aborted:required', function() { + ok(false, 'ajax:aborted:required should not run'); + }); + + submit_with_button(submit_button); +}); - App.short_timeout(); +asyncTest('blank required form input for non-remote form with "novalidate" attribute should not abort normal submission', 1, function() { + $(document).on('iframe:loading', function() { + ok(true, 'form should get submitted'); + }); + + var form = $('form[data-remote]') + .append($('')) + .removeAttr('data-remote') + .attr("novalidate","novalidate") + .trigger('submit'); + + setTimeout(function() { + start(); + }, 13); }); -test('before, loading, success, complete and after callbacks should be called', function() { - expect(5); - stop(App.ajax_timeout); +asyncTest('unchecked required checkbox should abort form submission', 1, function() { + var form = $('form[data-remote]') + .append($('')) + .removeAttr('data-remote') + .on('ujs:everythingStopped', function() { + ok(true, 'ujs:everythingStopped should run'); + }) + .trigger('submit'); + + setTimeout(function() { + start(); + }, 13); +}); - $('form') - .bind('ajax:before', function() { ok(true, 'ajax:before'); return true; }) - .bind('ajax:loading', function(arg) { ok(true, 'ajax:loading'); }) - .bind('ajax:success', function(arg) { ok(true, 'ajax:success'); }) - .bind('ajax:complete', function(arg) { ok(true, 'ajax:complete'); start(); }) - .bind('ajax:after', function() { ok(true, 'ajax:after'); }); +asyncTest('unchecked required radio should abort form submission', 1, function() { + var form = $('form[data-remote]') + .append($('')) + .append($('')) + .removeAttr('data-remote') + .on('ujs:everythingStopped', function() { + ok(true, 'ujs:everythingStopped should run'); + }) + .trigger('submit'); - $('form[data-remote]').trigger('submit'); + setTimeout(function() { + start(); + }, 13); }); -test('before, loading, error, complete and after callbacks should be called in case of error', function() { - expect(6); - $('form').attr('action', App.url('error')); - stop(App.ajax_timeout); +asyncTest('required radio should only require one to be checked', 1, function() { + $(document).on('iframe:loading', function() { + ok(true, 'form should get submitted'); + }); - $('form') - .bind('ajax:before', function() { ok(true, 'ajax:before'); return true; }) - .bind('ajax:loading', function(arg) { ok(true, 'ajax:loading'); }) - .bind('ajax:failure', function(e, xhr, status, error) { - ok(true, 'ajax:failure'); - equals(xhr.status, 403, 'status code should be 403'); + var form = $('form[data-remote]') + .append($('')) + .append($('')) + .removeAttr('data-remote') + .on('ujs:everythingStopped', function() { + ok(false, 'ujs:everythingStopped should not run'); }) - .bind('ajax:complete', function(arg) { ok(true, 'ajax:complete'); start(); }) - .bind('ajax:after', function() { ok(true, 'ajax:after'); }); + .find('#checkme').prop('checked', true) + .end() + .trigger('submit'); - $('form[data-remote]').trigger('submit'); + setTimeout(function() { + start(); + }, 13); }); +asyncTest('required radio should only require one to be checked if not all radios are required', 1, function() { + $(document).on('iframe:loading', function() { + ok(true, 'form should get submitted'); + }); + + var form = $('form[data-remote]') + // Check the radio that is not required + .append($('')) + // Check the radio that is not required + .append($('')) + // Only one needs to be required + .append($('')) + .removeAttr('data-remote') + .on('ujs:everythingStopped', function() { + ok(false, 'ujs:everythingStopped should not run'); + }) + .find('#checkme').prop('checked', true) + .end() + .trigger('submit'); + + setTimeout(function() { + start(); + }, 13); +}); + +function skipIt() { + // This test cannot work due to the security feature in browsers which makes the value + // attribute of file input fields readonly, so it cannot be set with default value. + // This is what the test would look like though if browsers let us automate this test. + asyncTest('non-blank file form input field should abort remote request, but submit normally', 5, function() { + var form = $('form[data-remote]') + .append($('')) + .on('ajax:beforeSend', function() { + ok(false, 'ajax:beforeSend should not run'); + }) + .on('iframe:loading', function() { + ok(true, 'form should get submitted'); + }) + .on('ajax:aborted:file', function(e,data) { + ok(data.length == 1, 'ajax:aborted:file event is passed all non-blank file inputs (jQuery objects)'); + ok(data.first().is('input[name="attachment"]') , 'ajax:aborted:file adds non-blank file input to data'); + ok(true, 'ajax:aborted:file event should run'); + }) + .trigger('submit'); + + setTimeout(function() { + form.find('input[type="file"]').val(''); + form.off('ajax:beforeSend'); + submit(); + }, 13); + }); + + asyncTest('file form input field should not abort remote request if file form input does not have a name attribute', 5, function() { + var form = $('form[data-remote]') + .append($('')) + .on('ajax:beforeSend', function() { + ok(true, 'ajax:beforeSend should run'); + }) + .on('iframe:loading', function() { + ok(true, 'form should get submitted'); + }) + .on('ajax:aborted:file', function(e,data) { + ok(false, 'ajax:aborted:file should not run'); + }) + .trigger('submit'); + + setTimeout(function() { + form.find('input[type="file"]').val(''); + form.off('ajax:beforeSend'); + submit(); + }, 13); + }); + + asyncTest('blank file input field should abort request entirely if handler bound to "ajax:aborted:file" event that returns false', 1, function() { + var form = $('form[data-remote]') + .append($('')) + .on('ajax:beforeSend', function() { + ok(false, 'ajax:beforeSend should not run'); + }) + .on('iframe:loading', function() { + ok(false, 'form should not get submitted'); + }) + .on('ajax:aborted:file', function() { + return false; + }) + .trigger('submit'); + + setTimeout(function() { + form.find('input[type="file"]').val(''); + form.off('ajax:beforeSend'); + submit(); + }, 13); + }); +} + +asyncTest('"ajax:beforeSend" can be observed and stopped with event delegation', 1, function() { + $(document).on('ajax:beforeSend', 'form[data-remote]', function() { + ok(true, 'ajax:beforeSend observed with event delegation'); + return false; + }); + + submit(function(form) { + form.off('ajax:send').on('ajax:send', function() { + ok(false, 'ajax:send should not run'); + }); + form.off('ajax:complete').on('ajax:complete', function() { + ok(false, 'ajax:complete should not run'); + }); + $(document).on('ajaxStop', function() { + start(); + }); + }); +}); + +asyncTest('"ajax:beforeSend", "ajax:send", "ajax:success" and "ajax:complete" are triggered', 9, function() { + submit(function(form) { + form.on('ajax:beforeSend', function(e, xhr, settings) { + ok(xhr.setRequestHeader, 'first argument to "ajax:beforeSend" should be an XHR object'); + equal(settings.url, '/echo', 'second argument to "ajax:beforeSend" should be a settings object'); + }); + form.on('ajax:send', function(e, xhr) { + ok(xhr.abort, 'first argument to "ajax:send" should be an XHR object'); + }); + form.on('ajax:success', function(e, data, status, xhr) { + ok(data.REQUEST_METHOD, 'first argument to ajax:success should be a data object'); + equal(status, 'success', 'second argument to ajax:success should be a status string'); + ok(xhr.getResponseHeader, 'third argument to "ajax:success" should be an XHR object'); + }); + form.on('ajax:complete', function(e, xhr, status) { + ok(xhr.getResponseHeader, 'first argument to "ajax:complete" should be an XHR object'); + equal(status, 'success', 'second argument to ajax:complete should be a status string'); + }); + }); +}); + +if(window.phantom !== undefined) { + asyncTest('"ajax:beforeSend", "ajax:send", "ajax:error" and "ajax:complete" are triggered on error', 7, function() { + submit(function(form) { + form.attr('action', '/error'); + form.on('ajax:beforeSend', function(arg) { ok(true, 'ajax:beforeSend') }); + form.on('ajax:send', function(arg) { ok(true, 'ajax:send') }); + form.on('ajax:error', function(e, xhr, status, error) { + ok(xhr.getResponseHeader, 'first argument to "ajax:error" should be an XHR object'); + equal(status, 'error', 'second argument to ajax:error should be a status string'); + // Firefox 8 returns "Forbidden " with trailing space + equal($.trim(error), 'Forbidden', 'third argument to ajax:error should be an HTTP status response'); + // Opera returns "0" for HTTP code + equal(xhr.status, window.opera ? 0 : 403, 'status code should be 403'); + }); + }); + }); +} + +// IF THIS TEST IS FAILING, TRY INCREASING THE TIMEOUT AT THE BOTTOM TO > 100 +asyncTest('binding to ajax callbacks via .on() triggers handlers properly', 4, function() { + $(document) + .on('ajax:beforeSend', 'form[data-remote]', function() { + ok(true, 'ajax:beforeSend handler is triggered'); + }) + .on('ajax:send', 'form[data-remote]', function() { + ok(true, 'ajax:send handler is triggered'); + }) + .on('ajax:complete', 'form[data-remote]', function() { + ok(true, 'ajax:complete handler is triggered'); + }) + .on('ajax:success', 'form[data-remote]', function() { + ok(true, 'ajax:success handler is triggered'); + }); + $('form[data-remote]').trigger('submit'); + + setTimeout(function() { + start(); + }, 63); +}); + +asyncTest('binding to ajax:send event to call jquery methods on ajax object', 2, function() { + $('form[data-remote]') + .bind('ajax:send', function(e, xhr) { + ok(true, 'event should fire'); + equal(typeof(xhr.abort), 'function', 'event should pass jqXHR object'); + xhr.abort(); + }) + .trigger('submit'); + + setTimeout(function() { start(); }, 35); +}); +})(); diff --git a/test/public/test/call-remote.js b/test/public/test/call-remote.js index 3e707527..0d9214a0 100644 --- a/test/public/test/call-remote.js +++ b/test/public/test/call-remote.js @@ -1,181 +1,188 @@ -var App = App || {}; +(function(){ -App.build_form = function(opt) { +function buildForm(attrs) { + attrs = $.extend({ action: '/echo', 'data-remote': 'true' }, attrs); - var defaults = { - 'data-remote': 'true' - }; + $('#qunit-fixture').append($('', attrs)) + .find('form').append($('')); +}; - var options = $.extend(defaults, opt); +module('call-remote'); - $('#fixtures').append($('', options)); +function submit(fn) { + $('form') + .on('ajax:success', fn) + .on('ajax:complete', function() { start() }) + .trigger('submit'); +} - $('#fixtures form').append($('', { - id: 'user_name', - type: 'text', - size: '30', - 'name': 'user_name', - 'value': 'john' - })); -}; +asyncTest('form method is read from "method" and not from "data-method"', 1, function() { + buildForm({ method: 'post', 'data-method': 'get' }); -module('call-remote', { - teardown: App.teardown + submit(function(e, data, status, xhr) { + App.assertPostRequest(data); + }); }); -test('method should be picked up from method attribute and not from data-method', function() { - expect(2); - App.build_form({ - 'method': 'post', - 'data-method': 'get', - 'action': App.url('update') +asyncTest('form method is not read from "data-method" attribute in case of missing "method"', 1, function() { + buildForm({ 'data-method': 'put' }); + + submit(function(e, data, status, xhr) { + App.assertGetRequest(data); }); - stop(App.ajax_timeout); +}); - $('form[data-remote]') - .live('ajax:success', function(e, data, status, xhr) { - App.assert_callback_invoked('ajax:success'); - var request_env = $.parseJSON(data)['request_env']; - App.assert_post_request(request_env); +asyncTest('form method is read from submit button "formmethod" if submit is triggered by that button', 1, function() { + var submitButton = $('') + buildForm({ method: 'post' }); - start(); + $('#qunit-fixture').find('form').append(submitButton) + .on('ajax:success', function(e, data, status, xhr) { + App.assertGetRequest(data); }) - .trigger('submit'); + .on('ajax:complete', function() { start() }); + + submitButton.trigger('click'); }); -test('method should be picked up from data-method attribute if method is missing', function() { - expect(2); +asyncTest('form default method is GET', 1, function() { + buildForm(); - App.build_form({ - 'data-method': 'post', - 'action': App.url('update') + submit(function(e, data, status, xhr) { + App.assertGetRequest(data); }); +}); - stop(App.ajax_timeout); - - $('form[data-remote]') - .live('ajax:success', function(e, data, status, xhr) { - App.assert_callback_invoked('ajax:success'); - var request_env = $.parseJSON(data)['request_env']; - App.assert_post_request(request_env); +asyncTest('form url is picked up from "action"', 1, function() { + buildForm({ method: 'post' }); - start(); - }) - .trigger('submit'); + submit(function(e, data, status, xhr) { + App.assertRequestPath(data, '/echo'); + }); }); -test('default method GET should be picked up if no method or data-method is supplied', function() { - expect(2); +asyncTest('form url is read from "action" not "href"', 1, function() { + buildForm({ method: 'post', href: '/echo2' }); - App.build_form({ - action: App.url('show') + submit(function(e, data, status, xhr) { + App.assertRequestPath(data, '/echo'); }); +}); - stop(App.ajax_timeout); - - $('form[data-remote]') - .live('ajax:success', function(e, data, status, xhr) { - App.assert_callback_invoked('ajax:success'); - var request_env = $.parseJSON(data)['request_env']; - App.assert_get_request(request_env); +asyncTest('form url is read from submit button "formaction" if submit is triggered by that button', 1, function() { + var submitButton = $('') + buildForm({ method: 'post', href: '/echo2' }); - start(); + $('#qunit-fixture').find('form').append(submitButton) + .on('ajax:success', function(e, data, status, xhr) { + App.assertRequestPath(data, '/echo'); }) - .trigger('submit'); + .on('ajax:complete', function() { start() }); + + submitButton.trigger('click'); }); -test('url should be picked up from action', function() { - expect(2); +asyncTest('prefer JS, but accept any format', 1, function() { + buildForm({ method: 'post' }); - App.build_form({ - 'action': App.url('show') + submit(function(e, data, status, xhr) { + var accept = data.HTTP_ACCEPT; + ok(accept.indexOf('*/*;q=0.5, text/javascript, application/javascript') === 0, 'Accept: ' + accept); }); +}); - stop(App.ajax_timeout); - - $('form[data-remote]') - .live('ajax:success', function(e, data, status, xhr) { - App.assert_callback_invoked('ajax:success'); - var request_env = $.parseJSON(data)['request_env']; - App.assert_request_path(request_env, '/show'); +asyncTest('accept application/json if "data-type" is json', 1, function() { + buildForm({ method: 'post', 'data-type': 'json' }); - start(); - }) - .trigger('submit'); + submit(function(e, data, status, xhr) { + equal(data.HTTP_ACCEPT, 'application/json, text/javascript, */*; q=0.01'); + }); }); -test('url should be picked up from action if both action and href are mentioned ', function() { - expect(2); +asyncTest('allow empty "data-remote" attribute', 1, function() { + var form = $('#qunit-fixture').append($('')).find('form'); - App.build_form({ - 'action': App.url('show'), - 'href': 'http://example.org' + submit(function() { + ok(true, 'form with empty "data-remote" attribute is also allowed'); }); - stop(App.ajax_timeout); - - $('form[data-remote]') - .live('ajax:success', function(e, data, status, xhr) { - App.assert_callback_invoked('ajax:success'); - var request_env = $.parseJSON(data)['request_env']; - App.assert_request_path(request_env, '/show'); +}); - start(); +asyncTest('allow empty form "action"', 1, function() { + var currentLocation, ajaxLocation; + + buildForm({ action: '' }); + + $('#qunit-fixture').find('form') + .on('ajax:beforeSend', function(e, xhr, settings) { + // Get current location (the same way jQuery does) + try { + currentLocation = location.href; + } catch(e) { + currentLocation = document.createElement( "a" ); + currentLocation.href = ""; + currentLocation = currentLocation.href; + } + currentLocation = currentLocation.replace(/\?$/, ''); + + // Actual location (strip out settings.data that jQuery serializes and appends) + // HACK: can no longer use settings.data below to see what was appended to URL, as of + // jQuery 1.6.3 (see http://bugs.jquery.com/ticket/10202 and https://github.com/jquery/jquery/pull/544) + ajaxLocation = settings.url.replace("user_name=john","").replace(/&$/, "").replace(/\?$/, ""); + equal(ajaxLocation.match(/^(.*)/)[1], currentLocation, 'URL should be current page by default'); + + // Prevent the request from actually getting sent to the current page and + // causing an error. + return false; }) .trigger('submit'); + + setTimeout(function() { start(); }, 13); }); -test('url should be picked up from href if no action is provided', function() { - expect(2); +asyncTest('sends CSRF token in custom header', 1, function() { + buildForm({ method: 'post' }); + $('#qunit-fixture').append(''); - App.build_form({ - 'href': App.url('show') + submit(function(e, data, status, xhr) { + equal(data.HTTP_X_CSRF_TOKEN, 'cf50faa3fe97702ca1ae', 'X-CSRF-Token header should be sent'); }); - stop(App.ajax_timeout); +}); +asyncTest('intelligently guesses crossDomain behavior when target URL has a different protocol and/or hostname', 1, function(e, xhr) { - $('form[data-remote]') - .live('ajax:success', function(e, data, status, xhr) { - App.assert_callback_invoked('ajax:success'); - var request_env = $.parseJSON(data)['request_env']; - App.assert_request_path(request_env, '/show'); + // Don't set data-cross-domain here, just set action to be a different domain than localhost + buildForm({ action: 'http://www.alfajango.com' }); + $('#qunit-fixture').append(''); - start(); + $('#qunit-fixture').find('form') + .on('ajax:beforeSend', function(e, xhr, settings) { + + equal(settings.crossDomain, true, 'crossDomain should be set to true'); + + // prevent request from actually getting sent off-domain + return false; }) .trigger('submit'); -}); - -test('exception should be thrown if both action and url are missing', function() { - expect(1); - var exception_was_raised = false; - App.build_form({}); + setTimeout(function() { start(); }, 13); +}); - try { - $('form[data-remote]').trigger('submit'); - } catch(err) { - exception_was_raised = true; - } +asyncTest('intelligently guesses crossDomain behavior when target URL consists of only a path', 1, function(e, xhr) { - ok(exception_was_raised, 'exception should have been raised'); -}); + // Don't set data-cross-domain here, just set action to be a different domain than localhost + buildForm({ action: '/just/a/path' }); + $('#qunit-fixture').append(''); -test('data should be availabe in JSON format if datat-type is json', function() { - expect(2); - App.build_form({ - 'method': 'post', - 'data-method': 'get', - 'data-type': 'json', - 'action': App.url('update') - }); - stop(App.ajax_timeout); + $('#qunit-fixture').find('form') + .on('ajax:beforeSend', function(e, xhr, settings) { - $('form[data-remote]') - .live('ajax:success', function(e, data, status, xhr) { - App.assert_callback_invoked('ajax:success'); - var request_env = data['request_env']; - App.assert_post_request(request_env); + equal(settings.crossDomain, false, 'crossDomain should be set to false'); - start(); + // prevent request from actually getting sent off-domain + return false; }) .trigger('submit'); + + setTimeout(function() { start(); }, 13); }); +})(); diff --git a/test/public/test/csrf-refresh.js b/test/public/test/csrf-refresh.js new file mode 100644 index 00000000..65642433 --- /dev/null +++ b/test/public/test/csrf-refresh.js @@ -0,0 +1,24 @@ +(function(){ + +module('csrf-refresh', {}); + +asyncTest('refresh all csrf tokens', 1, function() { + var correctToken = "cf50faa3fe97702ca1ae"; + + var form = $('') + var input = $('').attr({ type: 'hidden', name: 'authenticity_token', id: 'authenticity_token', value: 'foo' }) + input.appendTo(form) + + $('#qunit-fixture') + .append('') + .append('') + .append(form); + + $.rails.refreshCSRFTokens(); + currentToken = $('#qunit-fixture #authenticity_token').val(); + + start(); + equal(currentToken, correctToken); +}); + +})(); diff --git a/test/public/test/csrf-token.js b/test/public/test/csrf-token.js new file mode 100644 index 00000000..dfed3781 --- /dev/null +++ b/test/public/test/csrf-token.js @@ -0,0 +1,27 @@ +(function(){ + +module('csrf-token', {}); + +asyncTest('find csrf token', 1, function() { + var correctToken = "cf50faa3fe97702ca1ae"; + + $('#qunit-fixture').append(''); + + currentToken = $.rails.csrfToken(); + + start(); + equal(currentToken, correctToken); +}); + +asyncTest('find csrf param', 1, function() { + var correctParam = "authenticity_token"; + + $('#qunit-fixture').append(''); + + currentParam = $.rails.csrfParam(); + + start(); + equal(currentParam, correctParam); +}); + +})(); diff --git a/test/public/test/data-confirm.js b/test/public/test/data-confirm.js index f6249ed5..2e905d42 100644 --- a/test/public/test/data-confirm.js +++ b/test/public/test/data-confirm.js @@ -1,169 +1,265 @@ module('data-confirm', { - - teardown: App.teardown, - - setup: function() { - - $('#fixtures').append($('', { - href: App.url('show'), - 'data-remote': 'true', - 'data-confirm': 'Are you absolutely sure?', - text: 'my social security number' - })); - - $('#fixtures').append($('', { - 'data-confirm': App.confirmation_message, - 'data-remote': 'true', - href: App.url('show'), - name: 'submit', - type: 'submit', - value: 'Click me' - })); - - - $('#fixtures').append($(''); + form.append(button); + + App.checkEnabledState(button, 'Submit'); + + form.on('ajax:success', function(e, data) { + setTimeout(function() { + App.checkEnabledState(button, 'Submit'); + start(); + }, 13); + }); + form.trigger('submit'); + + App.checkDisabledState(button, 'submitting ...'); +}); + +asyncTest('form input[type=submit][data-disable-with] disables', 6, function(){ + var form = $('form:not([data-remote])'), input = form.find('input[type=submit]'); + + App.checkEnabledState(input, 'Submit'); + + // WEEIRDD: attaching this handler makes the test work in IE7 + $(document).on('iframe:loading', function(e, form) {}); + + $(document).on('iframe:loaded', function(e, data) { + setTimeout(function() { + App.checkDisabledState(input, 'submitting ...'); + start(); + }, 30); + }); + form.trigger('submit'); + + setTimeout(function() { + App.checkDisabledState(input, 'submitting ...'); + }, 30); +}); + +test('form input[type=submit][data-disable-with] re-enables when `pageshow` event is triggered', function(){ + var form = $('form:not([data-remote])'), input = form.find('input[type=submit]'); + + App.checkEnabledState(input, 'Submit'); + + // Emulate the disabled state without submitting the form at all, what is the + // state after going back on firefox after submitting a form. + // + // See https://github.com/rails/jquery-ujs/issues/357 + $.rails.disableFormElements(form); + + App.checkDisabledState(input, 'submitting ...'); + + $(window).trigger('pageshow'); + + App.checkEnabledState(input, 'Submit'); +}); + +asyncTest('form[data-remote] input[type=submit][data-disable-with] is replaced in ajax callback', 2, function(){ + var form = $('form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html(); + + form.on('ajax:success', function(){ + form.html(origFormContents); + + setTimeout(function(){ + var input = form.find('input[type=submit]'); + App.checkEnabledState(input, 'Submit'); + start(); + }, 30); + }).trigger('submit'); +}); + +asyncTest('form[data-remote] input[data-disable-with] is replaced with disabled field in ajax callback', 2, function(){ + var form = $('form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'), + newDisabledInput = input.clone().attr('disabled', 'disabled'); + + form.on('ajax:success', function(){ + input.replaceWith(newDisabledInput); + + setTimeout(function(){ + App.checkEnabledState(newDisabledInput, 'Submit'); + start(); + }, 30); + }).trigger('submit'); +}); + +asyncTest('form input[type=submit][data-disable-with] using "form" attribute disables', 6, function() { + var form = $('#not_remote'), input = $('input[form=not_remote]'); + App.checkEnabledState(input, 'Form Attr Submit'); + + // WEEIRDD: attaching this handler makes the test work in IE7 + $(document).on('iframe:loading', function(e, form) {}); + + $(document).on('iframe:loaded', function(e, data) { + setTimeout(function() { + App.checkDisabledState(input, 'form attr submitting'); + start(); + }, 30); + }); + form.trigger('submit'); + + setTimeout(function() { + App.checkDisabledState(input, 'form attr submitting'); + }, 30); + +}); + +asyncTest('form[data-remote] textarea[data-disable-with] attribute', 3, function() { + var form = $('form[data-remote]'), + textarea = $('').appendTo(form); + + form.on('ajax:success', function(e, data) { + setTimeout(function() { + equal(data.params.user_bio, 'born, lived, died.'); + start(); + }, 13); + }); + form.trigger('submit'); + + App.checkDisabledState(textarea, 'processing ...'); +}); + +asyncTest('a[data-disable-with] disables', 4, function() { + var link = $('a[data-disable-with]'); + + App.checkEnabledState(link, 'Click me'); + + link.trigger('click'); + App.checkDisabledState(link, 'clicking...'); + start(); +}); + +test('a[data-disable-with] re-enables when `pageshow` event is triggered', function() { + var link = $('a[data-disable-with]'); + + App.checkEnabledState(link, 'Click me'); + + link.trigger('click'); + App.checkDisabledState(link, 'clicking...'); + + $(window).trigger('pageshow'); + App.checkEnabledState(link, 'Click me'); +}); + +asyncTest('a[data-remote][data-disable-with] disables and re-enables', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true); + + App.checkEnabledState(link, 'Click me'); + + link + .on('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...'); + }) + .on('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 15); + }) + .trigger('click'); +}); + +asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:before` event is cancelled', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true); + + App.checkEnabledState(link, 'Click me'); + + link + .on('ajax:before', function() { + App.checkDisabledState(link, 'clicking...'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 30); +}); + +asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true); + + App.checkEnabledState(link, 'Click me'); + + link + .on('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 30); +}); + +asyncTest('a[data-remote][data-disable-with] re-enables when `ajax:error` event is triggered', 6, function() { + var link = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error'); + + App.checkEnabledState(link, 'Click me'); + + link + .on('ajax:beforeSend', function() { + App.checkDisabledState(link, 'clicking...'); + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 30); +}); + +asyncTest('form[data-remote] input|button|textarea[data-disable-with] does not disable when `ajax:beforeSend` event is cancelled', 8, function() { + var form = $('form[data-remote]'), + input = form.find('input:text'), + button = $('').appendTo(form), + textarea = $('').appendTo(form), + submit = $('').appendTo(form); + + form + .on('ajax:beforeSend', function() { + return false; + }) + .trigger('submit'); + + App.checkEnabledState(input, 'john'); + App.checkEnabledState(button, 'Submit'); + App.checkEnabledState(textarea, 'born, lived, died.'); + App.checkEnabledState(submit, 'Submit'); + + start(); +}); + +asyncTest('ctrl-clicking on a link does not disables the link', 6, function() { + var link = $('a[data-disable-with]'), e; + e = $.Event('click'); + e.metaKey = true; + + App.checkEnabledState(link, 'Click me'); + + link.trigger(e); + App.checkEnabledState(link, 'Click me'); + + e = $.Event('click'); + e.ctrlKey = true; + + link.trigger(e); + App.checkEnabledState(link, 'Click me'); + start(); +}); + +asyncTest('button[data-remote][data-disable-with] disables and re-enables', 6, function() { + var button = $('button[data-remote][data-disable-with]'); + + App.checkEnabledState(button, 'Click me'); + + button + .on('ajax:send', function() { + App.checkDisabledState(button, 'clicking...'); + }) + .on('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 15); + }) + .trigger('click'); +}); + +asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:before` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable-with]'); + + App.checkEnabledState(button, 'Click me'); + + button + .on('ajax:before', function() { + App.checkDisabledState(button, 'clicking...'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); +}); + +asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable-with]'); + + App.checkEnabledState(button, 'Click me'); + + button + .on('ajax:beforeSend', function() { + App.checkDisabledState(button, 'clicking...'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); +}); + +asyncTest('button[data-remote][data-disable-with] re-enables when `ajax:error` event is triggered', 6, function() { + var button = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error'); + + App.checkEnabledState(button, 'Click me'); + + button + .on('ajax:send', function() { + App.checkDisabledState(button, 'clicking...'); + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); +}); diff --git a/test/public/test/data-disable.js b/test/public/test/data-disable.js index 04a65c47..ed30b0ec 100644 --- a/test/public/test/data-disable.js +++ b/test/public/test/data-disable.js @@ -1,68 +1,326 @@ module('data-disable', { + setup: function() { + $('#qunit-fixture').append($('', { + action: '/echo', + 'data-remote': 'true', + method: 'post' + })) + .find('form') + .append($('')); - teardown: App.teardown, - setup: function() { + $('#qunit-fixture').append($('', { + action: '/echo', + method: 'post' + })) + .find('form:last') + // WEEIRDD: the form won't submit to an iframe if the button is name="submit" (??!) + .append($('')); - $('#fixtures').append($('', { - action: App.url('update'), - 'data-remote': 'true', - method: 'post' - })); + $('#qunit-fixture').append($('', { + text: 'Click me', + href: '/echo', + 'data-disable': 'true' + })); - $('form').append($('', { - id: 'user_name', - 'data-disable-with': 'processing ...', - type: 'text', - size: '30', - 'name': 'user_name', - 'value': 'john' - })); + $('#qunit-fixture').append($(''); + form.append(button); + + App.checkEnabledState(button, 'Submit'); + + form.on('ajax:success', function(e, data) { + setTimeout(function() { + App.checkEnabledState(button, 'Submit'); + start(); + }, 13) + }) + form.trigger('submit'); + + App.checkDisabledState(button, 'Submit'); + equal(button.data('ujs:enable-with'), undefined); +}); + +asyncTest('form input[type=submit][data-disable] disables', 6, function(){ + var form = $('form:not([data-remote])'), input = form.find('input[type=submit]'); + + App.checkEnabledState(input, 'Submit'); + + // WEEIRDD: attaching this handler makes the test work in IE7 + $(document).on('iframe:loading', function(e, form) {}); + + $(document).on('iframe:loaded', function(e, data) { + setTimeout(function() { + App.checkDisabledState(input, 'Submit'); + start(); + }, 30); + }); + form.trigger('submit'); + + setTimeout(function() { + App.checkDisabledState(input, 'Submit'); + }, 30); +}); + +asyncTest('form[data-remote] input[type=submit][data-disable] is replaced in ajax callback', 2, function(){ + var form = $('form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html(); + + form.on('ajax:success', function(){ + form.html(origFormContents); + + setTimeout(function(){ + var input = form.find('input[type=submit]'); + App.checkEnabledState(input, 'Submit'); + start(); + }, 30); + }).trigger('submit'); +}); + +asyncTest('form[data-remote] input[data-disable] is replaced with disabled field in ajax callback', 2, function(){ + var form = $('form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'), + newDisabledInput = input.clone().attr('disabled', 'disabled'); + + form.on('ajax:success', function(){ + input.replaceWith(newDisabledInput); + + setTimeout(function(){ + App.checkEnabledState(newDisabledInput, 'Submit'); + start(); + }, 30); + }).trigger('submit'); +}); + +asyncTest('form[data-remote] textarea[data-disable] attribute', 3, function() { + var form = $('form[data-remote]'), + textarea = $('').appendTo(form); + + form.on('ajax:success', function(e, data) { + setTimeout(function() { + equal(data.params.user_bio, 'born, lived, died.'); + start(); + }, 13) + }) + form.trigger('submit'); + + App.checkDisabledState(textarea, 'born, lived, died.'); +}); + +asyncTest('a[data-disable] disables', 5, function() { + var link = $('a[data-disable]'); + + App.checkEnabledState(link, 'Click me'); + + link.trigger('click'); + App.checkDisabledState(link, 'Click me'); + equal(link.data('ujs:enable-with'), undefined); + start(); +}); + +asyncTest('a[data-remote][data-disable] disables and re-enables', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true); + + App.checkEnabledState(link, 'Click me'); + + link + .on('ajax:send', function() { + App.checkDisabledState(link, 'Click me'); + }) + .on('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 15); + }) + .trigger('click'); +}); + +asyncTest('a[data-remote][data-disable] re-enables when `ajax:before` event is cancelled', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true); + + App.checkEnabledState(link, 'Click me'); + + link + .on('ajax:before', function() { + App.checkDisabledState(link, 'Click me'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 30); +}); + +asyncTest('a[data-remote][data-disable] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true); + + App.checkEnabledState(link, 'Click me'); + + link + .on('ajax:beforeSend', function() { + App.checkDisabledState(link, 'Click me'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 30); +}); + +asyncTest('a[data-remote][data-disable] re-enables when `ajax:error` event is triggered', 6, function() { + var link = $('a[data-disable]').attr('data-remote', true).attr('href', '/error'); - $('#fixtures').append($('', { - action: App.url('update'), - method: 'post' - })); + App.checkEnabledState(link, 'Click me'); - $('form:not([data-remote])').append($('', { - id: 'submit', - 'data-disable-with': 'submitting ...', - type: 'submit', - name: 'submit', - value: 'Submit' - })); + link + .on('ajax:send', function() { + App.checkDisabledState(link, 'Click me'); + }) + .trigger('click'); - } + setTimeout(function() { + App.checkEnabledState(link, 'Click me'); + start(); + }, 30); }); -test('triggering ajax callbacks on a form with data-disable attribute', function() { - expect(6); +asyncTest('form[data-remote] input|button|textarea[data-disable] does not disable when `ajax:beforeSend` event is cancelled', 8, function() { + var form = $('form[data-remote]'), + input = form.find('input:text'), + button = $('').appendTo(form), + textarea = $('').appendTo(form), + submit = $('').appendTo(form); - equals($('input:disabled').size(), 0, 'input field should not be disabled'); - equals($('input').val(), 'john', 'input field should have value given to it'); + form + .on('ajax:beforeSend', function() { + return false; + }) + .trigger('submit'); - $('form').trigger('ajax:before'); + App.checkEnabledState(input, 'john'); + App.checkEnabledState(button, 'Submit'); + App.checkEnabledState(textarea, 'born, lived, died.'); + App.checkEnabledState(submit, 'Submit'); - equals($('input:disabled').size(), 1, 'input field should be disabled'); - equals($('input:disabled').val(), 'processing ...', 'input field should have disabled value given to it'); + start(); +}); + +asyncTest('ctrl-clicking on a link does not disables the link', 6, function() { + var link = $('a[data-disable]'), e; + e = $.Event('click'); + e.metaKey = true; + + App.checkEnabledState(link, 'Click me'); + + link.trigger(e); + App.checkEnabledState(link, 'Click me'); + + e = $.Event('click'); + e.ctrlKey = true; + + link.trigger(e); + App.checkEnabledState(link, 'Click me'); + start(); +}); + +asyncTest('button[data-remote][data-disable] disables and re-enables', 6, function() { + var button = $('button[data-remote][data-disable]'); - $('form').trigger('ajax:complete'); + App.checkEnabledState(button, 'Click me'); - equals($('input:disabled').size(), 0, 'input field should not be disabled'); - equals($('input').val(), 'john', 'input field should have value given to it'); + button + .on('ajax:send', function() { + App.checkDisabledState(button, 'Click me'); + }) + .on('ajax:complete', function() { + setTimeout( function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 15); + }) + .trigger('click'); +}); + +asyncTest('button[data-remote][data-disable] re-enables when `ajax:before` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable]'); + + App.checkEnabledState(button, 'Click me'); + + button + .on('ajax:before', function() { + App.checkDisabledState(button, 'Click me'); + return false; + }) + .trigger('click'); + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); }); -test('clicking on non-ajax Submit input tag with data-disable-with attribute', function(){ - expect(4); +asyncTest('button[data-remote][data-disable] re-enables when `ajax:beforeSend` event is cancelled', 6, function() { + var button = $('button[data-remote][data-disable]'); + + App.checkEnabledState(button, 'Click me'); + + button + .on('ajax:beforeSend', function() { + App.checkDisabledState(button, 'Click me'); + return false; + }) + .trigger('click'); + + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); +}); - equals($('input:disabled').size(), 0, 'input field should not be disabled'); - equals($('input[type=submit]').val(), 'Submit', 'input field should have value given to it'); +asyncTest('button[data-remote][data-disable] re-enables when `ajax:error` event is triggered', 6, function() { + var button = $('a[data-disable]').attr('data-remote', true).attr('href', '/error'); - $('form:not([data-remote])').live('submit', function (e) { - e.preventDefault(); - }).trigger('submit'); + App.checkEnabledState(button, 'Click me'); - equals($('input:disabled').size(), 1, 'input field should be disabled'); - equals($('input:disabled').val(), 'submitting ...', 'input field should have disabled value given to it'); + button + .on('ajax:send', function() { + App.checkDisabledState(button, 'Click me'); + }) + .trigger('click'); + setTimeout(function() { + App.checkEnabledState(button, 'Click me'); + start(); + }, 30); }); diff --git a/test/public/test/data-method-iframe.js b/test/public/test/data-method-iframe.js deleted file mode 100644 index 06dcd12d..00000000 --- a/test/public/test/data-method-iframe.js +++ /dev/null @@ -1,24 +0,0 @@ -module('data-remote-iframe', { - - teardown: App.teardown, - - setup: function() { - - $('#fixtures-iframe').append($('', { - href: App.url('delete'), - 'data-method': 'delete', - text: 'Destroy' - })); - - } -}); - -test('clicking on a link with data-method attribute', function() { - expect(0); - stop(); - - $('a[data-method]') - .trigger('click'); - - App.timeout(); -}); diff --git a/test/public/test/data-method.js b/test/public/test/data-method.js index e76a6f2a..32b90d70 100644 --- a/test/public/test/data-method.js +++ b/test/public/test/data-method.js @@ -1,45 +1,75 @@ -module('data-remote'); +(function(){ -test('clicking on a link with data-method attribute', function() { - expect(1); - stop(App.ajax_timeout); +module('data-method', { + setup: function() { + $('#qunit-fixture').append($('', { + href: '/echo', 'data-method': 'delete', text: 'destroy!' + })); + }, + teardown: function() { + $(document).off('iframe:loaded'); + } +}); + +function submit(fn, options) { + $(document).on('iframe:loaded', function(e, data) { + fn(data); + start(); + }); - var iframe = $('#fixtures-iframe iframe'); + $('#qunit-fixture').find('a') + .trigger('click'); +} - iframeCallback = function() { - var data = iframe.contents().find('body').text(); - equals(data, "/delete was invoked with delete verb. params is {\"_method\"=>\"delete\"}", 'iframe should have proper response message'); +asyncTest('link with "data-method" set to "delete"', 3, function() { + submit(function(data) { + equal(data.REQUEST_METHOD, 'DELETE'); + strictEqual(data.params.authenticity_token, undefined); + strictEqual(data.HTTP_X_CSRF_TOKEN, undefined); + }); +}); - start(); - }; +asyncTest('link with "data-method" and CSRF', 1, function() { + $('#qunit-fixture') + .append('') + .append(''); - //Nothing to do. Just wait for iframe to load and do its thing. And then verify - if(iframe[0].loaded) { - iframeCallback(); - } else { - iframe.live("load", iframeCallback); - } + submit(function(data) { + equal(data.params.authenticity_token, 'cf50faa3fe97702ca1ae'); + }); +}); + +asyncTest('link "target" should be carried over to generated form', 1, function() { + $('a[data-method]').attr('target', 'super-special-frame'); + submit(function(data) { + equal(data.params._target, 'super-special-frame'); + }); }); +asyncTest('link with "data-method" and cross origin', 1, function() { + var data = {}; -test('clicking on a link with data-method attribute and csrf', function() { - expect(1); - stop(App.ajax_timeout); + $('#qunit-fixture') + .append('') + .append(''); - var iframe = $('#fixtures-iframe-csrf iframe'); + $(document).on('submit', 'form', function(e) { + $(e.currentTarget).serializeArray().map(function(item) { + data[item.name] = item.value; + }); - var iframeCallback = function() { - var data = iframe.contents().find('body').text(); - equals(data, "/delete was invoked with delete verb. params is {\"_method\"=>\"delete\", \"authenticity_token\"=>\"cf50faa3fe97702ca1ae\"}", - 'iframe should be proper response message'); + return false; + }); - start(); - }; + var link = $('#qunit-fixture').find('a'); - //Nothing to do. Just wait for iframe to load and do its thing. And then verify - if(iframe[0].loaded) { - iframeCallback(); - } else { - iframe.live("load", iframeCallback); - } + link.attr('href', 'http://www.alfajango.com'); + + link.trigger('click'); + + start(); + + notEqual(data.authenticity_token, 'cf50faa3fe97702ca1ae'); }); + +})(); diff --git a/test/public/test/data-remote.js b/test/public/test/data-remote.js index cc64c8fa..215d8217 100644 --- a/test/public/test/data-remote.js +++ b/test/public/test/data-remote.js @@ -1,92 +1,361 @@ module('data-remote', { + setup: function() { + $('#qunit-fixture') + .append($('', { + href: '/echo', + 'data-remote': 'true', + 'data-params': 'data1=value1&data2=value2', + text: 'my address' + })) + .append($('