Remove rollup and test machinery for rails-ujs (#50535)

This leaves only the final compiled targets in place.
This commit is contained in:
David Heinemeier Hansson 2024-01-02 16:49:36 +01:00 committed by GitHub
parent 66c174557a
commit 8397eb24da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 14 additions and 4356 deletions

@ -9,7 +9,7 @@ jobs:
lint:
runs-on: ubuntu-latest
env:
BUNDLE_WITHOUT: db:job:cable:storage:ujs
BUNDLE_WITHOUT: db:job:cable:storage
steps:
- uses: actions/checkout@v4
@ -33,7 +33,7 @@ jobs:
python -m pip install --upgrade pip
pip install codespell==2.1.0
- name: Check spelling with codespell
run: codespell --ignore-words=codespell.txt --skip="./vendor/bundle,./actionview/test/ujs/public/vendor/qunit.js,./actiontext/app/assets/javascripts/trix.js,./yarn.lock" || exit 1
run: codespell --ignore-words=codespell.txt --skip="./vendor/bundle,./actiontext/app/assets/javascripts/trix.js,./yarn.lock" || exit 1
- run: tools/railspect changelogs .
- run: tools/railspect configuration .

@ -8,7 +8,7 @@ permissions:
env:
APP_NAME: devrails
APP_PATH: dev/devrails
BUNDLE_WITHOUT: db:job:cable:storage:ujs
BUNDLE_WITHOUT: db:job:cable:storage
jobs:
rails-new-docker:

@ -82,8 +82,8 @@ for setup instructions.
IMPORTANT: Several gems have JavaScript components that are released as npm
packages, so you must have Node.js installed, have an npm account (npmjs.com),
and be a package owner for `@rails/actioncable`, `@rails/actiontext`,
`@rails/activestorage`, and `@rails/ujs`. You can check this by making sure your
and be a package owner for `@rails/actioncable`, `@rails/actiontext`, and
`@rails/activestorage`. You can check this by making sure your
npm user (`npm whoami`) is listed as an owner (`npm owner ls <pkg>`) of each
package. Do not release until you're set up with npm!

@ -1,23 +0,0 @@
{
"extends": "eslint:recommended",
"globals": {
"__esm": "readonly"
},
"rules": {
"semi": ["error", "never"],
"quotes": ["error", "double"],
"no-unused-vars": ["error", { "vars": "all", "args": "none" }]
},
"plugins": [
"import"
],
"env": {
"browser": true,
"es6": true,
"jquery": true
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
}
}

@ -1,6 +1,4 @@
/lib/assets/compiled/
/test/ujs/compiled/
/log/
/test/fixtures/public/absolute/
/test/ujs/log/
/tmp/

@ -1,9 +0,0 @@
== Running UJS tests
Run the tests in headless mode by running:
rake test:ujs
To run the tests in a browser, start the Rails UJS server by running:
rake ujs:server

@ -30,46 +30,6 @@ namespace :test do
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
desc "Run tests for rails-ujs"
task :ujs do
system("npm run lint")
exit $?.exitstatus unless $?.success?
begin
listen_host = "localhost"
listen_port = "4567"
FileUtils.mkdir_p("log")
pid = File.open("log/test.log", "w") do |f|
spawn(*%W(rackup test/ujs/config.ru -o #{listen_host} -p #{listen_port} -s puma), out: f, err: f, pgroup: true)
end
start_time = Time.now
loop do
break if system("lsof -i :4567", 1 => File::NULL)
if Time.now - start_time > 5
puts "Failed to start puma after 5 seconds"
puts
puts File.read("log/test.log")
exit 1
end
sleep 0.2
end
# Decode the obfuscate environment variables
decoded_environment_variables = Hash[*Base64.decode64(ENV.fetch("ENCODED", "")).split(/[ =]/)]
system(decoded_environment_variables, "npm", "test")
status = $?.exitstatus
ensure
Process.kill("KILL", -pid) if pid
FileUtils.rm_rf("log")
end
exit status
end
namespace :integration do
# Active Record Integration Tests
Rake::TestTask.new(:active_record) do |t|
@ -91,14 +51,6 @@ namespace :test do
end
end
namespace :ujs do
desc "Starts the test server"
task :server do
spawn("bundle", "exec", "rackup", "test/ujs/config.ru", "-p", "4567", "-s", "puma")
system("npm", "test", "--", "--no-single-run", "--browsers", "Chrome")
end
end
task :lines do
load File.expand_path("../tools/line_statistics", __dir__)
files = FileList["lib/**/*.rb"]

@ -1,20 +0,0 @@
Copyright (c) Rails Core team
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

@ -1,61 +0,0 @@
# Ruby on Rails unobtrusive scripting adapter
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. Documentation about the various supported `data` attributes is [available here][ujsdocs]. In Rails, they are added by the framework's template helpers.
## Optional prerequisites
Note that the `data` attributes this library adds are a feature of HTML5. If you're not targeting HTML5, these attributes may make your HTML to fail [validation][validator]. However, this shouldn't create any issues for web browsers or other user agents.
## Installation
### Bun
bun add @rails/ujs
### npm
npm install @rails/ujs --save
### Yarn
yarn add @rails/ujs
Ensure that `.yarnclean` does not include `assets` if you use [yarn autoclean](https://yarnpkg.com/lang/en/docs/cli/autoclean/).
## Usage
### Asset pipeline
In a conventional Rails application that uses the asset pipeline, require `rails-ujs` in your `application.js` manifest:
```javascript
//= require rails-ujs
```
### ES2015+
If you're using a JavaScript bundler, add the following to your main JS file:
```javascript
import Rails from "@rails/ujs"
Rails.start()
```
## How to run tests
Run `bundle exec rake ujs:server` first, and then run the web tests by visiting http://localhost:4567 in your browser.
## License
rails-ujs is released under the [MIT License](MIT-LICENSE).
[data]: https://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"
[validator]: https://validator.w3.org/
[csrf]: https://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html
[ujsdocs]: https://github.com/rails/jquery-ujs/wiki

@ -1,33 +0,0 @@
import { fire, stopEverything } from "../utils/event"
const handleConfirmWithRails = (rails) => function(e) {
if (!allowAction(this, rails)) { stopEverything(e) }
}
// Default confirm dialog, may be overridden with custom confirm dialog in Rails.confirm
const confirm = (message, element) => window.confirm(message)
// 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.
var allowAction = function(element, rails) {
let callback
const message = element.getAttribute("data-confirm")
if (!message) { return true }
let answer = false
if (fire(element, "confirm")) {
try { answer = rails.confirm(message, element) } catch(error) { /* do nothing */ }
callback = fire(element, "confirm:complete", [answer])
}
return answer && callback
}
export { handleConfirmWithRails, confirm }

@ -1,128 +0,0 @@
import {
linkDisableSelector,
buttonDisableSelector,
formDisableSelector,
formEnableSelector,
formSubmitSelector
} from "../utils/constants"
import { matches, getData, setData } from "../utils/dom"
import { stopEverything } from "../utils/event"
import { formElements } from "../utils/form"
import { isContentEditable } from "../utils/dom"
const handleDisabledElement = function(e) {
const element = this
if (element.disabled) { stopEverything(e) }
}
// Unified function to enable an element (link, button and form)
const enableElement = (e) => {
let element
if (e instanceof Event) {
if (isXhrRedirect(e)) { return }
element = e.target
} else {
element = e
}
if (isContentEditable(element)) {
return
}
if (matches(element, linkDisableSelector)) {
return enableLinkElement(element)
} else if (matches(element, buttonDisableSelector) || matches(element, formEnableSelector)) {
return enableFormElement(element)
} else if (matches(element, formSubmitSelector)) {
return enableFormElements(element)
}
}
// Unified function to disable an element (link, button and form)
const disableElement = (e) => {
const element = e instanceof Event ? e.target : e
if (isContentEditable(element)) {
return
}
if (matches(element, linkDisableSelector)) {
return disableLinkElement(element)
} else if (matches(element, buttonDisableSelector) || matches(element, formDisableSelector)) {
return disableFormElement(element)
} else if (matches(element, formSubmitSelector)) {
return disableFormElements(element)
}
}
// Replace element's HTML with the 'data-disable-with' after storing original html
// and prevent clicking on it
var disableLinkElement = function(element) {
if (getData(element, "ujs:disabled")) { return }
const replacement = element.getAttribute("data-disable-with")
if (replacement != null) {
setData(element, "ujs:enable-with", element.innerHTML) // store enabled state
element.innerHTML = replacement
}
element.addEventListener("click", stopEverything) // prevent further clicking
return setData(element, "ujs:disabled", true)
}
// Restore element to its original state which was disabled by 'disableLinkElement' above
var enableLinkElement = function(element) {
const originalText = getData(element, "ujs:enable-with")
if (originalText != null) {
element.innerHTML = originalText // set to old enabled state
setData(element, "ujs:enable-with", null) // clean up cache
}
element.removeEventListener("click", stopEverything) // enable element
return setData(element, "ujs:disabled", null)
}
// 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
var disableFormElements = form => formElements(form, formDisableSelector).forEach(disableFormElement)
var disableFormElement = function(element) {
if (getData(element, "ujs:disabled")) { return }
const replacement = element.getAttribute("data-disable-with")
if (replacement != null) {
if (matches(element, "button")) {
setData(element, "ujs:enable-with", element.innerHTML)
element.innerHTML = replacement
} else {
setData(element, "ujs:enable-with", element.value)
element.value = replacement
}
}
element.disabled = true
return setData(element, "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
var enableFormElements = form => formElements(form, formEnableSelector).forEach(element => enableFormElement(element))
var enableFormElement = function(element) {
const originalText = getData(element, "ujs:enable-with")
if (originalText != null) {
if (matches(element, "button")) {
element.innerHTML = originalText
} else {
element.value = originalText
}
setData(element, "ujs:enable-with", null) // clean up cache
}
element.disabled = false
return setData(element, "ujs:disabled", null)
}
var isXhrRedirect = function(event) {
const xhr = event.detail ? event.detail[0] : undefined
return xhr && xhr.getResponseHeader("X-Xhr-Redirect")
}
export { handleDisabledElement, enableElement, disableElement }

@ -1,43 +0,0 @@
import { isCrossDomain } from "../utils/ajax"
import * as csrf from "../utils/csrf"
import { stopEverything } from "../utils/event"
import { isContentEditable } from "../utils/dom"
// Handles "data-method" on links such as:
// <a href="/users/5" data-method="delete" rel="nofollow" data-confirm="Are you sure?">Delete</a>
const handleMethodWithRails = (rails) => function(e) {
const link = this
const method = link.getAttribute("data-method")
if (!method) { return }
if (isContentEditable(this)) {
return
}
const href = rails.href(link)
const csrfToken = csrf.csrfToken()
const csrfParam = csrf.csrfParam()
const form = document.createElement("form")
let formContent = `<input name='_method' value='${method}' type='hidden' />`
if (csrfParam && csrfToken && !isCrossDomain(href)) {
formContent += `<input name='${csrfParam}' value='${csrfToken}' type='hidden' />`
}
// Must trigger submit by click on a button, else "submit" event handler won't work!
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit
formContent += "<input type=\"submit\" />"
form.method = "post"
form.action = href
form.target = link.target
form.innerHTML = formContent
form.style.display = "none"
document.body.appendChild(form)
form.querySelector("[type=\"submit\"]").click()
stopEverything(e)
}
export { handleMethodWithRails }

@ -1,109 +0,0 @@
import { formSubmitSelector, buttonClickSelector, inputChangeSelector } from "../utils/constants"
import { ajax, isCrossDomain } from "../utils/ajax"
import { matches, getData, setData } from "../utils/dom"
import { fire, stopEverything } from "../utils/event"
import { serializeElement } from "../utils/form"
import { isContentEditable } from "../utils/dom"
// Checks "data-remote" if true to handle the request through a XHR request.
const isRemote = function(element) {
const value = element.getAttribute("data-remote")
return (value != null) && (value !== "false")
}
// Submits "remote" forms and links with ajax
const handleRemoteWithRails = (rails) => function(e) {
let data, method, url
const element = this
if (!isRemote(element)) { return true }
if (!fire(element, "ajax:before")) {
fire(element, "ajax:stopped")
return false
}
if (isContentEditable(element)) {
fire(element, "ajax:stopped")
return false
}
const withCredentials = element.getAttribute("data-with-credentials")
const dataType = element.getAttribute("data-type") || "script"
if (matches(element, formSubmitSelector)) {
// memoized value from clicked submit button
const button = getData(element, "ujs:submit-button")
method = getData(element, "ujs:submit-button-formmethod") || element.getAttribute("method") || "get"
url = getData(element, "ujs:submit-button-formaction") || element.getAttribute("action") || location.href
// strip query string if it's a GET request
if (method.toUpperCase() === "GET") { url = url.replace(/\?.*$/, "") }
if (element.enctype === "multipart/form-data") {
data = new FormData(element)
if (button != null) { data.append(button.name, button.value) }
} else {
data = serializeElement(element, button)
}
setData(element, "ujs:submit-button", null)
setData(element, "ujs:submit-button-formmethod", null)
setData(element, "ujs:submit-button-formaction", null)
} else if (matches(element, buttonClickSelector) || matches(element, inputChangeSelector)) {
method = element.getAttribute("data-method")
url = element.getAttribute("data-url")
data = serializeElement(element, element.getAttribute("data-params"))
} else {
method = element.getAttribute("data-method")
url = rails.href(element)
data = element.getAttribute("data-params")
}
ajax({
type: method || "GET",
url,
data,
dataType,
// stopping the "ajax:beforeSend" event will cancel the ajax request
beforeSend(xhr, options) {
if (fire(element, "ajax:beforeSend", [xhr, options])) {
return fire(element, "ajax:send", [xhr])
} else {
fire(element, "ajax:stopped")
return false
}
},
success(...args) { return fire(element, "ajax:success", args) },
error(...args) { return fire(element, "ajax:error", args) },
complete(...args) { return fire(element, "ajax:complete", args) },
crossDomain: isCrossDomain(url),
withCredentials: (withCredentials != null) && (withCredentials !== "false")
})
stopEverything(e)
}
const formSubmitButtonClick = function(e) {
const button = this
const {
form
} = button
if (!form) { return }
// Register the pressed submit button
if (button.name) { setData(form, "ujs:submit-button", {name: button.name, value: button.value}) }
// Save attributes from button
setData(form, "ujs:formnovalidate-button", button.formNoValidate)
setData(form, "ujs:submit-button-formaction", button.getAttribute("formaction"))
return setData(form, "ujs:submit-button-formmethod", button.getAttribute("formmethod"))
}
const preventInsignificantClick = function(e) {
const link = this
const method = (link.getAttribute("data-method") || "GET").toUpperCase()
const data = link.getAttribute("data-params")
const metaClick = e.metaKey || e.ctrlKey
const insignificantMetaClick = metaClick && (method === "GET") && !data
const nonPrimaryMouseClick = (e.button != null) && (e.button !== 0)
if (nonPrimaryMouseClick || insignificantMetaClick) { e.stopImmediatePropagation() }
}
export { handleRemoteWithRails, formSubmitButtonClick, preventInsignificantClick }

@ -1,164 +0,0 @@
import {
linkClickSelector,
buttonClickSelector,
inputChangeSelector,
formSubmitSelector,
formInputClickSelector,
formDisableSelector,
formEnableSelector,
fileInputSelector,
linkDisableSelector,
buttonDisableSelector
} from "./utils/constants"
import { ajax, href, isCrossDomain } from "./utils/ajax"
import { cspNonce, loadCSPNonce } from "./utils/csp"
import { csrfToken, csrfParam, CSRFProtection, refreshCSRFTokens } from "./utils/csrf"
import { matches, getData, setData, $ } from "./utils/dom"
import { fire, stopEverything, delegate } from "./utils/event"
import { serializeElement, formElements } from "./utils/form"
import { confirm, handleConfirmWithRails } from "./features/confirm"
import { handleDisabledElement, enableElement, disableElement } from "./features/disable"
import { handleMethodWithRails } from "./features/method"
import { handleRemoteWithRails, formSubmitButtonClick, preventInsignificantClick } from "./features/remote"
const Rails = {
$,
ajax,
buttonClickSelector,
buttonDisableSelector,
confirm,
cspNonce,
csrfToken,
csrfParam,
CSRFProtection,
delegate,
disableElement,
enableElement,
fileInputSelector,
fire,
formElements,
formEnableSelector,
formDisableSelector,
formInputClickSelector,
formSubmitButtonClick,
formSubmitSelector,
getData,
handleDisabledElement,
href,
inputChangeSelector,
isCrossDomain,
linkClickSelector,
linkDisableSelector,
loadCSPNonce,
matches,
preventInsignificantClick,
refreshCSRFTokens,
serializeElement,
setData,
stopEverything
}
// needs to be able to call Rails.confirm in case its overridden
const handleConfirm = handleConfirmWithRails(Rails)
Rails.handleConfirm = handleConfirm
// needs to be able to call Rails.href in case its overridden
const handleMethod = handleMethodWithRails(Rails)
Rails.handleMethod = handleMethod
// needs to be able to call Rails.href in case its overridden
const handleRemote = handleRemoteWithRails(Rails)
Rails.handleRemote = handleRemote
const start = function() {
// Cut down on the number of issues from people inadvertently including
// rails-ujs twice by detecting and raising an error when it happens.
if (window._rails_loaded) { throw new Error("rails-ujs has already been loaded!") }
// 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.addEventListener("pageshow", function() {
$(formEnableSelector).forEach(function(el) {
if (getData(el, "ujs:disabled")) {
enableElement(el)
}
})
$(linkDisableSelector).forEach(function(el) {
if (getData(el, "ujs:disabled")) {
enableElement(el)
}
})
})
delegate(document, linkDisableSelector, "ajax:complete", enableElement)
delegate(document, linkDisableSelector, "ajax:stopped", enableElement)
delegate(document, buttonDisableSelector, "ajax:complete", enableElement)
delegate(document, buttonDisableSelector, "ajax:stopped", enableElement)
delegate(document, linkClickSelector, "click", preventInsignificantClick)
delegate(document, linkClickSelector, "click", handleDisabledElement)
delegate(document, linkClickSelector, "click", handleConfirm)
delegate(document, linkClickSelector, "click", disableElement)
delegate(document, linkClickSelector, "click", handleRemote)
delegate(document, linkClickSelector, "click", handleMethod)
delegate(document, buttonClickSelector, "click", preventInsignificantClick)
delegate(document, buttonClickSelector, "click", handleDisabledElement)
delegate(document, buttonClickSelector, "click", handleConfirm)
delegate(document, buttonClickSelector, "click", disableElement)
delegate(document, buttonClickSelector, "click", handleRemote)
delegate(document, inputChangeSelector, "change", handleDisabledElement)
delegate(document, inputChangeSelector, "change", handleConfirm)
delegate(document, inputChangeSelector, "change", handleRemote)
delegate(document, formSubmitSelector, "submit", handleDisabledElement)
delegate(document, formSubmitSelector, "submit", handleConfirm)
delegate(document, formSubmitSelector, "submit", handleRemote)
// Normal mode submit
// Slight timeout so that the submit button gets properly serialized
delegate(document, formSubmitSelector, "submit", e => setTimeout((() => disableElement(e)), 13))
delegate(document, formSubmitSelector, "ajax:send", disableElement)
delegate(document, formSubmitSelector, "ajax:complete", enableElement)
delegate(document, formInputClickSelector, "click", preventInsignificantClick)
delegate(document, formInputClickSelector, "click", handleDisabledElement)
delegate(document, formInputClickSelector, "click", handleConfirm)
delegate(document, formInputClickSelector, "click", formSubmitButtonClick)
document.addEventListener("DOMContentLoaded", refreshCSRFTokens)
document.addEventListener("DOMContentLoaded", loadCSPNonce)
return window._rails_loaded = true
}
Rails.start = start
// For backward compatibility
if (typeof jQuery !== "undefined" && jQuery && jQuery.ajax) {
if (jQuery.rails) { throw new Error("If you load both jquery_ujs and rails-ujs, use rails-ujs only.") }
jQuery.rails = Rails
jQuery.ajaxPrefilter(function(options, originalOptions, xhr) {
if (!options.crossDomain) { return CSRFProtection(xhr) }
})
}
// This block is to maintain backwards compatibility with the existing
// difference between what happens in a bundler and what happens using a
// sprockets compiler. In the sprockets case, Rails.start() is called
// automatically, but it is not in the ESModule case.
if (__esm == false && typeof exports !== "object" && typeof module === "undefined") {
// The coffeescript bundle would set this at the very top. The Rollup bundle
// doesn't set this until the entire bundle has finished running, so we need
// to make sure its set before firing the rails:attachBindings event for
// backwards compatibility.
window.Rails = Rails
if (fire(document, "rails:attachBindings")) {
start()
}
}
export default Rails

@ -1,119 +0,0 @@
import { cspNonce } from "./csp"
import { CSRFProtection } from "./csrf"
const AcceptHeaders = {
"*": "*/*",
text: "text/plain",
html: "text/html",
xml: "application/xml, text/xml",
json: "application/json, text/javascript",
script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
}
const ajax = (options) => {
options = prepareOptions(options)
var xhr = createXHR(options, function() {
const response = processResponse(xhr.response != null ? xhr.response : xhr.responseText, xhr.getResponseHeader("Content-Type"))
if (Math.floor(xhr.status / 100) === 2) {
if (typeof options.success === "function") {
options.success(response, xhr.statusText, xhr)
}
} else {
if (typeof options.error === "function") {
options.error(response, xhr.statusText, xhr)
}
}
return (typeof options.complete === "function" ? options.complete(xhr, xhr.statusText) : undefined)
})
if (options.beforeSend && !options.beforeSend(xhr, options)) {
return false
}
if (xhr.readyState === XMLHttpRequest.OPENED) {
return xhr.send(options.data)
}
}
var prepareOptions = function(options) {
options.url = options.url || location.href
options.type = options.type.toUpperCase()
// append data to url if it's a GET request
if ((options.type === "GET") && options.data) {
if (options.url.indexOf("?") < 0) {
options.url += "?" + options.data
} else {
options.url += "&" + options.data
}
}
// Use "*" as default dataType
if (!(options.dataType in AcceptHeaders)) { options.dataType = "*" }
options.accept = AcceptHeaders[options.dataType]
if (options.dataType !== "*") { options.accept += ", */*; q=0.01" }
return options
}
var createXHR = function(options, done) {
const xhr = new XMLHttpRequest()
// Open and set up xhr
xhr.open(options.type, options.url, true)
xhr.setRequestHeader("Accept", options.accept)
// Set Content-Type only when sending a string
// Sending FormData will automatically set Content-Type to multipart/form-data
if (typeof options.data === "string") {
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
}
if (!options.crossDomain) {
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
// Add X-CSRF-Token
CSRFProtection(xhr)
}
xhr.withCredentials = !!options.withCredentials
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) { return done(xhr) }
}
return xhr
}
var processResponse = function(response, type) {
if ((typeof response === "string") && (typeof type === "string")) {
if (type.match(/\bjson\b/)) {
try { response = JSON.parse(response) } catch (error) { /* do nothing */ }
} else if (type.match(/\b(?:java|ecma)script\b/)) {
const script = document.createElement("script")
script.setAttribute("nonce", cspNonce())
script.text = response
document.head.appendChild(script).parentNode.removeChild(script)
} else if (type.match(/\b(xml|html|svg)\b/)) {
const parser = new DOMParser()
type = type.replace(/;.+/, "") // remove something like ';charset=utf-8'
try { response = parser.parseFromString(response, type) } catch (error1) { /* do nothing */ }
}
}
return response
}
// Default way to get an element's href. May be overridden at Rails.href.
const href = element => element.href
// Determines if the request is a cross domain request.
const isCrossDomain = function(url) {
const originAnchor = document.createElement("a")
originAnchor.href = location.href
const urlAnchor = document.createElement("a")
try {
urlAnchor.href = url
// 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
}
}
export { ajax, href, isCrossDomain }

@ -1,45 +0,0 @@
// Link elements bound by rails-ujs
const linkClickSelector = "a[data-confirm], a[data-method], a[data-remote]:not([disabled]), a[data-disable-with], a[data-disable]"
// Button elements bound by rails-ujs
const buttonClickSelector = {
selector: "button[data-remote]:not([form]), button[data-confirm]:not([form])",
exclude: "form button"
}
// Select elements bound by rails-ujs
const inputChangeSelector = "select[data-remote], input[data-remote], textarea[data-remote]"
// Form elements bound by rails-ujs
const formSubmitSelector = "form:not([data-turbo=true])"
// Form input elements bound by rails-ujs
const 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
const formDisableSelector = "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
const formEnableSelector = "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 file input elements
const fileInputSelector = "input[name][type=file]:not([disabled])"
// Link onClick disable selector with possible re-enable after remote submission
const linkDisableSelector = "a[data-disable-with], a[data-disable]"
// Button onClick disable selector with possible re-enable after remote submission
const buttonDisableSelector = "button[data-remote][data-disable-with], button[data-remote][data-disable]"
export {
linkClickSelector,
buttonClickSelector,
inputChangeSelector,
formSubmitSelector,
formInputClickSelector,
formDisableSelector,
formEnableSelector,
fileInputSelector,
linkDisableSelector,
buttonDisableSelector
}

@ -1,11 +0,0 @@
let nonce = null
const loadCSPNonce = () => {
const metaTag = document.querySelector("meta[name=csp-nonce]")
return nonce = metaTag && metaTag.content
}
// Returns the Content-Security-Policy nonce for inline scripts.
const cspNonce = () => nonce || loadCSPNonce()
export { cspNonce, loadCSPNonce }

@ -1,30 +0,0 @@
import { $ } from "./dom"
// Up-to-date Cross-Site Request Forgery token
const csrfToken = () => {
const meta = document.querySelector("meta[name=csrf-token]")
return meta && meta.content
}
// URL param that must contain the CSRF token
const csrfParam = () => {
const meta = document.querySelector("meta[name=csrf-param]")
return meta && meta.content
}
// Make sure that every Ajax request sends the CSRF token
const CSRFProtection = (xhr) => {
const token = csrfToken()
if (token) { return xhr.setRequestHeader("X-CSRF-Token", token) }
}
// Make sure that all forms have actual up-to-date tokens (cached forms contain old ones)
const refreshCSRFTokens = () => {
const token = csrfToken()
const param = csrfParam()
if (token && param) {
return $("form input[name=\"" + param + "\"]").forEach(input => input.value = token)
}
}
export { csrfToken, csrfParam, CSRFProtection, refreshCSRFTokens }

@ -1,52 +0,0 @@
const m = Element.prototype.matches ||
Element.prototype.matchesSelector ||
Element.prototype.mozMatchesSelector ||
Element.prototype.msMatchesSelector ||
Element.prototype.oMatchesSelector ||
Element.prototype.webkitMatchesSelector
// Checks if the given native dom element matches the selector
// element::
// native DOM element
// selector::
// CSS selector string or
// a JavaScript object with `selector` and `exclude` properties
// Examples: "form", { selector: "form", exclude: "form[data-remote='true']"}
const matches = function(element, selector) {
if (selector.exclude) {
return m.call(element, selector.selector) && !m.call(element, selector.exclude)
} else {
return m.call(element, selector)
}
}
// get and set data on a given element using "expando properties"
// See: https://developer.mozilla.org/en-US/docs/Glossary/Expando
const EXPANDO = "_ujsData"
const getData = (element, key) => element[EXPANDO] ? element[EXPANDO][key] : undefined
const setData = function(element, key, value) {
if (!element[EXPANDO]) { element[EXPANDO] = {} }
return element[EXPANDO][key] = value
}
// a wrapper for document.querySelectorAll
// returns an Array
const $ = selector => Array.prototype.slice.call(document.querySelectorAll(selector))
const isContentEditable = function(element) {
var isEditable = false
do {
if(element.isContentEditable) {
isEditable = true
break
}
element = element.parentElement
} while(element)
return isEditable
}
export { matches, getData, setData, $, isContentEditable }

@ -1,82 +0,0 @@
import { matches } from "./dom"
let preventDefault
// Polyfill for CustomEvent in IE9+
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#Polyfill
let {
CustomEvent
} = window
if (typeof CustomEvent !== "function") {
CustomEvent = function(event, params) {
const evt = document.createEvent("CustomEvent")
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail)
return evt
}
CustomEvent.prototype = window.Event.prototype;
// Fix setting `defaultPrevented` when `preventDefault()` is called
// http://stackoverflow.com/questions/23349191/event-preventdefault-is-not-working-in-ie-11-for-custom-events
({ preventDefault } = CustomEvent.prototype)
CustomEvent.prototype.preventDefault = function() {
const result = preventDefault.call(this)
if (this.cancelable && !this.defaultPrevented) {
Object.defineProperty(this, "defaultPrevented", {get() { return true }})
}
return result
}
}
// Triggers a custom event on an element and returns false if the event result is false
// obj::
// a native DOM element
// name::
// string that corresponds to the event you want to trigger
// e.g. 'click', 'submit'
// data::
// data you want to pass when you dispatch an event
const fire = (obj, name, data) => {
const event = new CustomEvent(
name, {
bubbles: true,
cancelable: true,
detail: data
}
)
obj.dispatchEvent(event)
return !event.defaultPrevented
}
// Helper function, needed to provide consistent behavior in IE
const stopEverything = (e) => {
fire(e.target, "ujs:everythingStopped")
e.preventDefault()
e.stopPropagation()
e.stopImmediatePropagation()
}
// Delegates events
// to a specified parent `element`, which fires event `handler`
// for the specified `selector` when an event of `eventType` is triggered
// element::
// parent element that will listen for events e.g. document
// selector::
// CSS selector; or an object that has `selector` and `exclude` properties (see: Rails.matches)
// eventType::
// string representing the event e.g. 'submit', 'click'
// handler::
// the event handler to be called
const delegate = (element, selector, eventType, handler) => element.addEventListener(eventType, function(e) {
let {
target
} = e
while (!!(target instanceof Element) && !matches(target, selector)) { target = target.parentNode }
if (target instanceof Element && (handler.call(target, e) === false)) {
e.preventDefault()
e.stopPropagation()
}
})
export { fire, stopEverything, delegate }

@ -1,43 +0,0 @@
import { matches } from "./dom"
const toArray = e => Array.prototype.slice.call(e)
const serializeElement = (element, additionalParam) => {
let inputs = [element]
if (matches(element, "form")) { inputs = toArray(element.elements) }
const params = []
inputs.forEach(function(input) {
if (!input.name || input.disabled) { return }
if (matches(input, "fieldset[disabled] *")) { return }
if (matches(input, "select")) {
toArray(input.options).forEach(function(option) {
if (option.selected) { params.push({name: input.name, value: option.value}) }
})
} else if (input.checked || (["radio", "checkbox", "submit"].indexOf(input.type) === -1)) {
params.push({name: input.name, value: input.value})
}
})
if (additionalParam) { params.push(additionalParam) }
return params.map(function(param) {
if (param.name) {
return `${encodeURIComponent(param.name)}=${encodeURIComponent(param.value)}`
} else {
return param
}}).join("&")
}
// 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
const formElements = (form, selector) => {
if (matches(form, "form")) {
return toArray(form.elements).filter(el => matches(el, selector))
} else {
return toArray(form.querySelectorAll(selector))
}
}
export { serializeElement, formElements }

@ -1,66 +0,0 @@
// Karma configuration for running the UJS tests
const config = {
browsers: ["ChromeHeadless"],
frameworks: ["qunit"],
files: [
"test/ujs/compiled/test.js",
],
client: {
clearContext: false,
qunit: {
showUI: true
}
},
singleRun: true,
autoWatch: false,
captureTimeout: 180000,
browserDisconnectTimeout: 180000,
browserDisconnectTolerance: 3,
browserNoActivityTimeout: 300000,
proxies: {
'/echo': 'http://localhost:4567/echo',
'/error': 'http://localhost:4567/error'
}
}
if (process.env.CI) {
config.customLaunchers = {
sl_chrome: sauce("chrome", "latest", "Windows 10")
}
config.browsers = Object.keys(config.customLaunchers)
config.reporters = ["dots", "saucelabs"]
config.sauceLabs = {
testName: "Rails UJS",
retryLimit: 3,
build: buildId(),
}
function sauce(browserName, version, platform) {
const options = {
base: "SauceLabs",
browserName: browserName.toString(),
version: version.toString(),
}
if (platform) {
options.platform = platform.toString()
}
return options
}
function buildId() {
const { BUILDKITE_JOB_ID } = process.env
return BUILDKITE_JOB_ID
? `Buildkite ${BUILDKITE_JOB_ID}`
: ""
}
}
module.exports = function(karmaConfig) {
karmaConfig.set(config)
}

@ -17,7 +17,7 @@ module CsrfHelper
# You don't need to use these tags for regular forms as they generate their own hidden fields.
#
# For Ajax requests other than GETs, extract the "csrf-token" from the meta-tag and send as the
# +X-CSRF-Token+ HTTP header. If you are using rails-ujs, this happens automatically.
# +X-CSRF-Token+ HTTP header.
#
def csrf_meta_tags
if defined?(protect_against_forgery?) && protect_against_forgery?

@ -522,25 +522,6 @@ def radio_button_tag(name, value, *args)
# submit_tag "Edit", class: "edit_button"
# # => <input class="edit_button" data-disable-with="Edit" name="commit" type="submit" value="Edit" />
#
# ==== Deprecated: \Rails UJS attributes
#
# Prior to \Rails 7, \Rails shipped with the JavaScript library called @rails/ujs on by default. Following \Rails 7,
# this library is no longer on by default. This library integrated with the following options:
#
# * <tt>confirm: 'question?'</tt> - If present the unobtrusive JavaScript
# drivers will provide a prompt with the question specified. If the user accepts,
# the form is processed normally, otherwise no action is taken.
# * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a
# disabled version of the submit button when the form is submitted. This feature is
# provided by the unobtrusive JavaScript driver. To disable this feature for a single submit tag
# pass <tt>:data => { disable_with: false }</tt> Defaults to value attribute.
#
# submit_tag "Complete sale", data: { disable_with: "Submitting..." }
# # => <input name="commit" data-disable-with="Submitting..." type="submit" value="Complete sale" />
#
# submit_tag "Save", data: { confirm: "Are you sure?" }
# # => <input name='commit' type='submit' value='Save' data-disable-with="Save" data-confirm="Are you sure?" />
#
def submit_tag(value = "Save changes", options = {})
options = options.deep_stringify_keys
tag_options = { "type" => "submit", "name" => "commit", "value" => value }.update(options)
@ -582,26 +563,6 @@ def submit_tag(value = "Save changes", options = {})
# # <strong>Ask me!</strong>
# # </button>
#
# ==== Deprecated: \Rails UJS attributes
#
# Prior to \Rails 7, \Rails shipped with a JavaScript library called @rails/ujs on by default. Following \Rails 7,
# this library is no longer on by default. This library integrated with the following options:
#
# * <tt>confirm: 'question?'</tt> - If present, the
# unobtrusive JavaScript drivers will provide a prompt with
# the question specified. If the user accepts, the form is
# processed normally, otherwise no action is taken.
# * <tt>:disable_with</tt> - Value of this parameter will be
# used as the value for a disabled version of the submit
# button when the form is submitted. This feature is provided
# by the unobtrusive JavaScript driver.
#
# button_tag "Save", data: { confirm: "Are you sure?" }
# # => <button name="button" type="submit" data-confirm="Are you sure?">Save</button>
#
# button_tag "Checkout", data: { disable_with: "Please wait..." }
# # => <button data-disable-with="Please wait..." name="button" type="submit">Checkout</button>
#
def button_tag(content_or_options = nil, options = nil, &block)
if content_or_options.is_a? Hash
options = content_or_options

@ -195,42 +195,6 @@ def _filtered_referrer # :nodoc:
# link_to "Visit Other Site", "https://rubyonrails.org/", data: { turbo_confirm: "Are you sure?" }
# # => <a href="https://rubyonrails.org/" data-turbo-confirm="Are you sure?">Visit Other Site</a>
#
# ==== Deprecated: \Rails UJS Attributes
#
# Prior to \Rails 7, \Rails shipped with a JavaScript library called <tt>@rails/ujs</tt> on by default. Following \Rails 7,
# this library is no longer on by default. This library integrated with the following options:
#
# * <tt>method: symbol of HTTP verb</tt> - This modifier will dynamically
# create an HTML form and immediately submit the form for processing using
# the HTTP verb specified. Useful for having links perform a POST operation
# in dangerous actions like deleting a record (which search bots can follow
# while spidering your site). Supported verbs are <tt>:post</tt>, <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>.
# Note that if the user has JavaScript disabled, the request will fall back
# to using GET. If <tt>href: '#'</tt> is used and the user has JavaScript
# disabled clicking the link will have no effect. If you are relying on the
# POST behavior, you should check for it in your controller's action by using
# the request object's methods for <tt>post?</tt>, <tt>delete?</tt>, <tt>patch?</tt>, or <tt>put?</tt>.
# * <tt>remote: true</tt> - This will allow <tt>@rails/ujs</tt>
# to make an Ajax request to the URL in question instead of following
# the link.
#
# <tt>@rails/ujs</tt> also integrated with the following +:data+ options:
#
# * <tt>confirm: "question?"</tt> - This will allow <tt>@rails/ujs</tt>
# to prompt with the question specified (in this case, the
# resulting text would be <tt>question?</tt>). If the user accepts, the
# link is processed normally, otherwise no action is taken.
# * <tt>:disable_with</tt> - Value of this parameter will be used as the
# name for a disabled version of the link.
#
# ===== \Rails UJS Examples
#
# link_to "Remove Profile", profile_path(@profile), method: :delete
# # => <a href="/profiles/1" rel="nofollow" data-method="delete">Remove Profile</a>
#
# link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }
# # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a>
#
def link_to(name = nil, options = nil, html_options = nil, &block)
html_options, options, name = options, name, block if block_given?
options ||= {}
@ -328,32 +292,6 @@ def link_to(name = nil, options = nil, html_options = nil, &block)
# # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6" autocomplete="off"/>
# # </form>"
#
# ==== Deprecated: \Rails UJS Attributes
#
# Prior to \Rails 7, \Rails shipped with a JavaScript library called <tt>@rails/ujs</tt> on by default. Following \Rails 7,
# this library is no longer on by default. This library integrated with the following options:
#
# * <tt>:remote</tt> - If set to true, will allow <tt>@rails/ujs</tt> to control the
# submit behavior. By default this behavior is an Ajax submit.
#
# <tt>@rails/ujs</tt> also integrated with the following +:data+ options:
#
# * <tt>confirm: "question?"</tt> - This will allow <tt>@rails/ujs</tt>
# to prompt with the question specified (in this case, the
# resulting text would be <tt>question?</tt>). If the user accepts, the
# button is processed normally, otherwise no action is taken.
# * <tt>:disable_with</tt> - Value of this parameter will be
# used as the value for a disabled version of the submit
# button when the form is submitted.
#
# ===== \Rails UJS Examples
#
# <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %>
# # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json">
# # <button type="submit">Create</button>
# # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6" autocomplete="off"/>
# # </form>"
#
def button_to(name = nil, options = nil, html_options = nil, &block)
html_options, options = options, name if block_given?
html_options ||= {}

@ -1,49 +0,0 @@
{
"name": "@rails/ujs",
"version": "7.2.0-alpha",
"description": "Ruby on Rails unobtrusive scripting adapter",
"main": "app/assets/javascripts/rails-ujs.js",
"module": "app/assets/javascripts/rails-ujs.esm.js",
"files": [
"app/assets/javascripts/*.js"
],
"directories": {
"test": "test"
},
"scripts": {
"build": "rollup --config rollup.config.js",
"pretest": "rollup --config rollup.config.test.js",
"test": "karma start",
"lint": "eslint app/javascript && eslint test/ujs/public/test"
},
"repository": {
"type": "git",
"url": "rails/rails"
},
"contributors": [
"Stephen St. Martin",
"Steve Schwartz",
"Dangyi Liu",
"All contributors"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/rails/rails/issues"
},
"homepage": "https://rubyonrails.org/",
"devDependencies": {
"@rollup/plugin-commonjs": "^19.0.1",
"@rollup/plugin-node-resolve": "^11.0.1",
"@rollup/plugin-replace": "^5.0.4",
"eslint": "^4.19.1",
"eslint-plugin-import": "^2.23.4",
"jquery": "^2.2.0",
"karma": "^3.1.1",
"karma-chrome-launcher": "^2.2.0",
"karma-qunit": "^2.1.0",
"karma-sauce-launcher": "^1.2.0",
"qunit": "^2.8.0",
"rollup": "^2.53.3",
"rollup-plugin-terser": "^7.0.2"
}
}

@ -1,60 +0,0 @@
import { terser } from "rollup-plugin-terser"
import replace from "@rollup/plugin-replace"
const banner = `
/*
Unobtrusive JavaScript
https://github.com/rails/rails/blob/main/actionview/app/javascript
Released under the MIT license
*/
`
const terserOptions = {
mangle: false,
compress: false,
format: {
beautify: true,
indent_level: 2,
comments: function (node, comment) {
if (comment.type == "comment2") {
// multiline comment
return comment.value.includes("Released under the MIT license")
}
}
}
}
export default [
{
input: "app/javascript/rails-ujs/index.js",
output: {
file: "app/assets/javascripts/rails-ujs.js",
format: "umd",
name: "Rails",
banner,
},
plugins: [
replace({
preventAssignment: true,
values: { __esm: false },
}),
terser(terserOptions),
]
},
{
input: "app/javascript/rails-ujs/index.js",
output: {
file: "app/assets/javascripts/rails-ujs.esm.js",
format: "es",
banner,
},
plugins: [
replace({
preventAssignment: true,
values: { __esm: true },
}),
terser(terserOptions),
]
}
]

@ -1,23 +0,0 @@
// Rollup configuration for compiling the UJS tests
import commonjs from "@rollup/plugin-commonjs"
import replace from "@rollup/plugin-replace"
import resolve from "@rollup/plugin-node-resolve"
export default {
input: "test/ujs/src/test.js",
output: {
file: "test/ujs/compiled/test.js",
format: "iife"
},
plugins: [
replace({
preventAssignment: true,
values: { __esm: false }, // false because the tests expects start() to be called automatically
}),
resolve(),
commonjs()
]
}

@ -1,16 +0,0 @@
# frozen_string_literal: true
class JavascriptPackageTest < ActiveSupport::TestCase
def test_compiled_code_is_in_sync_with_source_code
compiled_files = %w[
app/assets/javascripts/rails-ujs.js
app/assets/javascripts/rails-ujs.esm.js
].map do |file|
Pathname(file).expand_path("#{__dir__}/..")
end
assert_no_changes -> { compiled_files.map(&:read) } do
system "yarn build", exception: true
end
end
end

@ -1,6 +0,0 @@
# frozen_string_literal: true
$LOAD_PATH.unshift __dir__
require "server"
run UJS::Server

@ -1,21 +0,0 @@
env:
browser: true
extends: eslint:recommended
rules:
no-undef: off
no-unused-vars: off
indent: off
linebreak-style: ['error', 'unix']
quotes: ['error', 'single']
semi: ['error', 'never']
no-shadow: ['error'] # Prevent potential errors
no-console: 'off'
# styles
space-before-function-paren: ['error', 'never']
space-before-blocks: 'error'
brace-style: ['error', '1tbs', { allowSingleLine: true }]
key-spacing: 'error'
array-bracket-spacing: 'error'
comma-spacing: 'error'
comma-dangle: 'off'
eol-last: 'error'

@ -1,27 +0,0 @@
import $ from 'jquery'
import Rails from '../../../../app/javascript/rails-ujs/index'
QUnit.module('call-ajax', {
beforeEach: function() {
$('#qunit-fixture')
.append($('<a />', { href: '#' }))
}
})
QUnit.test('call ajax without "ajax:beforeSend"', function(assert) {
const done = assert.async()
var link = $('#qunit-fixture a')
link.bindNative('click', function() {
Rails.ajax({
type: 'get',
url: '/',
success: function() {
assert.ok(true, 'calling request in ajax:success')
done()
}
})
})
link.triggerNative('click')
})

@ -1,266 +0,0 @@
import $ from 'jquery'
QUnit.module('call-remote-callbacks', {
beforeEach: function() {
$('#qunit-fixture').append($('<form />', {
action: '/echo', method: 'get', 'data-remote': 'true'
}))
},
afterEach: function() {
$(document).undelegate('form[data-remote]', 'ajax:beforeSend')
$(document).undelegate('form[data-remote]', 'ajax:before')
$(document).undelegate('form[data-remote]', 'ajax:send')
$(document).undelegate('form[data-remote]', 'ajax:complete')
$(document).undelegate('form[data-remote]', 'ajax:success')
$(document).unbind('iframe:loading')
}
})
function submit(fn) {
var form = $('#qunit-fixture form')
if (fn) fn(form)
form.triggerNative('submit')
}
QUnit.test('modifying form fields with "ajax:before" sends modified data in request', function(assert) {
const done = assert.async()
$('form[data-remote]')
.append($('<input type="text" name="user_name" value="john">'))
.append($('<input type="text" name="removed_user_name" value="john">'))
.bindNative('ajax:before', function() {
var form = $(this)
form
.append($('<input />', {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.bindNative('ajax:success', function(e, data, status, xhr) {
assert.equal(data.params.user_name, 'steve', 'modified field value should have been submitted')
assert.equal(data.params.other_user_name, 'jonathan', 'added field value should have been submitted')
assert.equal(data.params.removed_user_name, undefined, 'removed field value should be undefined')
done()
})
})
})
QUnit.test('modifying data("type") with "ajax:before" requests new dataType in request', function(assert) {
$('form[data-remote]').data('type', 'html')
.bindNative('ajax:before', function() {
this.setAttribute('data-type', 'xml')
})
submit(function(form) {
form.bindNative('ajax:beforeSend', function(e, xhr, settings) {
assert.equal(settings.dataType, 'xml', 'modified dataType should have been requested')
})
})
})
QUnit.test('setting data("with-credentials",true) with "ajax:before" uses new setting in request', function(assert) {
$('form[data-remote]').data('with-credentials', false)
.bindNative('ajax:before', function() {
this.setAttribute('data-with-credentials', true)
})
submit(function(form) {
form.bindNative('ajax:beforeSend', function(e, xhr, settings) {
assert.equal(settings.withCredentials, true, 'setting modified in ajax:before should have forced withCredentials request')
})
})
})
QUnit.test('stopping the "ajax:beforeSend" event aborts the request', function(assert) {
const done = assert.async()
submit(function(form) {
form.bindNative('ajax:beforeSend', function(e) {
assert.ok(true, 'aborting request in ajax:beforeSend')
e.preventDefault()
})
form.unbind('ajax:send').bindNative('ajax:send', function() {
assert.ok(false, 'ajax:send should not run')
})
form.bindNative('ajax:error', function(e, response, status, xhr) {
assert.ok(false, 'ajax:error should not run')
})
form.bindNative('ajax:complete', function() {
assert.ok(false, 'ajax:complete should not run')
})
})
setTimeout(function() { done() }, 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.
QUnit.test('non-blank file form input field should abort remote request, but submit normally', function(assert) {
var form = $('form[data-remote]')
.append($('<input type="file" name="attachment" value="default.png">'))
.bindNative('ajax:beforeSend', function() {
ok(false, 'ajax:beforeSend should not run')
})
.bind('iframe:loading', function() {
ok(true, 'form should get submitted')
})
.bindNative('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')
})
.triggerNative('submit')
setTimeout(function() {
form.find('input[type="file"]').val('')
form.unbind('ajax:beforeSend')
submit()
}, 13)
})
QUnit.test('file form input field should not abort remote request if file form input does not have a name attribute', function(assert) {
var form = $('form[data-remote]')
.append($('<input type="file" value="default.png">'))
.bindNative('ajax:beforeSend', function() {
ok(true, 'ajax:beforeSend should run')
})
.bind('iframe:loading', function() {
ok(true, 'form should get submitted')
})
.bindNative('ajax:aborted:file', function(e, data) {
ok(false, 'ajax:aborted:file should not run')
})
.triggerNative('submit')
setTimeout(function() {
form.find('input[type="file"]').val('')
form.unbind('ajax:beforeSend')
submit()
}, 13)
})
QUnit.test('blank file input field should abort request entirely if handler bound to "ajax:aborted:file" event that returns false', function(assert) {
var form = $('form[data-remote]')
.append($('<input type="file" name="attachment" value="default.png">'))
.bindNative('ajax:beforeSend', function() {
ok(false, 'ajax:beforeSend should not run')
})
.bind('iframe:loading', function() {
ok(false, 'form should not get submitted')
})
.bindNative('ajax:aborted:file', function(e) {
e.preventDefault()
})
.triggerNative('submit')
setTimeout(function() {
form.find('input[type="file"]').val('')
form.unbind('ajax:beforeSend')
submit()
}, 13)
})
}
QUnit.test('"ajax:beforeSend" can be observed and stopped with event delegation', function(assert) {
const done = assert.async()
$(document).delegate('form[data-remote]', 'ajax:beforeSend', function(e) {
assert.ok(true, 'ajax:beforeSend observed with event delegation')
e.preventDefault()
})
submit(function(form) {
form.unbind('ajax:send').bindNative('ajax:send', function() {
assert.ok(false, 'ajax:send should not run')
})
form.bindNative('ajax:complete', function() {
assert.ok(false, 'ajax:complete should not run')
})
})
setTimeout(function() { done() }, 13)
})
QUnit.test('"ajax:beforeSend", "ajax:send", "ajax:success" and "ajax:complete" are triggered', function(assert) {
const done = assert.async(4)
submit(function(form) {
form.bindNative('ajax:beforeSend', function(e, xhr, settings) {
assert.ok(xhr.setRequestHeader, 'first argument to "ajax:beforeSend" should be an XHR object')
assert.equal(settings.url, '/echo', 'second argument to "ajax:beforeSend" should be a settings object')
done()
})
form.bindNative('ajax:send', function(e, xhr) {
assert.ok(xhr.abort, 'first argument to "ajax:send" should be an XHR object')
done()
})
form.bindNative('ajax:success', function(e, data, status, xhr) {
assert.ok(data.REQUEST_METHOD, 'first argument to ajax:success should be a data object')
assert.equal(status, 'OK', 'second argument to ajax:success should be a status string')
assert.ok(xhr.getResponseHeader, 'third argument to "ajax:success" should be an XHR object')
done()
})
form.bindNative('ajax:complete', function(e, xhr, status) {
assert.ok(xhr.getResponseHeader, 'first argument to "ajax:complete" should be an XHR object')
assert.equal(status, 'OK', 'second argument to ajax:complete should be a status string')
done()
})
})
})
QUnit.test('"ajax:beforeSend", "ajax:send", "ajax:error" and "ajax:complete" are triggered on error', function(assert) {
const done = assert.async(4)
submit(function(form) {
form.attr('action', '/error')
form.bindNative('ajax:beforeSend', function(arg) {
assert.ok(true, 'ajax:beforeSend')
done()
})
form.bindNative('ajax:send', function(arg) {
assert.ok(true, 'ajax:send')
done()
})
form.bindNative('ajax:error', function(e, response, status, xhr) {
assert.equal(response, '', 'first argument to ajax:error should be an HTTP status response')
assert.equal(status, 'Forbidden', 'second argument to ajax:error should be a status string')
assert.ok(xhr.getResponseHeader, 'third argument to "ajax:error" should be an XHR object')
// Opera returns "0" for HTTP code
assert.equal(xhr.status, window.opera ? 0 : 403, 'status code should be 403')
done()
})
form.bindNative('ajax:complete', function(e, xhr, status) {
assert.ok(xhr.getResponseHeader, 'first argument to "ajax:complete" should be an XHR object')
assert.equal(status, 'Forbidden', 'second argument to ajax:complete should be a status string')
done()
})
})
})
QUnit.test('binding to ajax callbacks via .delegate() triggers handlers properly', function(assert) {
const done = assert.async(4)
$(document)
.delegate('form[data-remote]', 'ajax:beforeSend', function() {
assert.ok(true, 'ajax:beforeSend handler is triggered')
done()
})
.delegate('form[data-remote]', 'ajax:send', function() {
assert.ok(true, 'ajax:send handler is triggered')
done()
})
.delegate('form[data-remote]', 'ajax:success', function() {
assert.ok(true, 'ajax:success handler is triggered')
done()
})
.delegate('form[data-remote]', 'ajax:complete', function() {
assert.ok(true, 'ajax:complete handler is triggered')
done()
})
$('form[data-remote]').triggerNative('submit')
})

@ -1,354 +0,0 @@
import $ from 'jquery'
function buildForm(attrs) {
attrs = $.extend({ action: '/echo', 'data-remote': 'true' }, attrs)
$('#qunit-fixture').append($('<form />', attrs))
.find('form').append($('<input type="text" name="user_name" value="john">'))
}
QUnit.module('call-remote')
function submit(fn) {
$('#qunit-fixture form')
.bindNative('ajax:success', fn)
.triggerNative('submit')
}
QUnit.test('form method is read from "method" and not from "data-method"', function(assert) {
const done = assert.async()
buildForm({ method: 'post', 'data-method': 'get' })
submit(function(e, data, status, xhr) {
assert.postRequest(data)
done()
})
})
QUnit.test('form method is not read from "data-method" attribute in case of missing "method"', function(assert) {
const done = assert.async()
buildForm({ 'data-method': 'put' })
submit(function(e, data, status, xhr) {
assert.getRequest(data)
done()
})
})
QUnit.test('form method is read from submit button "formmethod" if submit is triggered by that button', function(assert) {
const done = assert.async()
var submitButton = $('<input type="submit" formmethod="get">')
buildForm({ method: 'post' })
$('#qunit-fixture').find('form').append(submitButton)
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.getRequest(data)
})
.bindNative('ajax:complete', function() { done() })
submitButton.triggerNative('click')
})
QUnit.test('form default method is GET', function(assert) {
const done = assert.async()
buildForm()
submit(function(e, data, status, xhr) {
assert.getRequest(data)
done()
})
})
QUnit.test('form URL is picked up from "action"', function(assert) {
const done = assert.async()
buildForm({ method: 'post' })
submit(function(e, data, status, xhr) {
assert.requestPath(data, '/echo')
done()
})
})
QUnit.test('form URL is read from "action" not "href"', function(assert) {
const done = assert.async()
buildForm({ method: 'post', href: '/echo2' })
submit(function(e, data, status, xhr) {
assert.requestPath(data, '/echo')
done()
})
})
QUnit.test('form URL is read from submit button "formaction" if submit is triggered by that button', function(assert) {
const done = assert.async()
var submitButton = $('<input type="submit" formaction="/echo">')
buildForm({ method: 'post', href: '/echo2' })
$('#qunit-fixture').find('form').append(submitButton)
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.requestPath(data, '/echo')
})
.bindNative('ajax:complete', function() { done() })
submitButton.triggerNative('click')
})
QUnit.test('prefer JS, but accept any format', function(assert) {
const done = assert.async()
buildForm({ method: 'post' })
submit(function(e, data, status, xhr) {
var accept = data.HTTP_ACCEPT
assert.ok(accept.match(/text\/javascript.+\*\/\*/), 'Accept: ' + accept)
done()
})
})
QUnit.test('JS code should be executed', function(assert) {
const done = assert.async()
buildForm({ method: 'post', 'data-type': 'script' })
window.callback = function() {
assert.ok(true, 'remote code should be run')
window.callback = null
done()
}
$('form').append('<input type="text" name="content_type" value="text/javascript">')
$('form').append('<input type="text" name="content" value="window.callback()">')
submit()
})
QUnit.test('ecmascript code should be executed', function(assert) {
const done = assert.async()
window.callback = function() {
assert.ok(true, 'remote code should be run')
window.callback = null
done()
}
buildForm({ method: 'post', 'data-type': 'script' })
$('form').append('<input type="text" name="content_type" value="application/ecmascript">')
$('form').append('<input type="text" name="content" value="window.callback()">')
submit()
})
QUnit.test('execution of JS code does not modify current DOM', function(assert) {
const done = assert.async()
var docLength, newDocLength
function getDocLength() {
return document.documentElement.outerHTML.length
}
buildForm({ method: 'post', 'data-type': 'script' })
$('form').append('<input type="text" name="content_type" value="text/javascript">')
$('form').append('<input type="text" name="content" value="\'remote code should be run\'">')
docLength = getDocLength()
submit(function() {
newDocLength = getDocLength()
assert.ok(docLength === newDocLength, 'executed JS should not present in the document')
done()
})
})
QUnit.test('HTML document should be parsed', function(assert) {
const done = assert.async()
buildForm({ method: 'post', 'data-type': 'html' })
$('form').append('<input type="text" name="content_type" value="text/html">')
$('form').append('<input type="text" name="content" value="<p>hello</p>">')
submit(function(e, data, status, xhr) {
assert.ok(data instanceof HTMLDocument, 'returned data should be an HTML document')
done()
})
})
QUnit.test('XML document should be parsed', function(assert) {
const done = assert.async()
buildForm({ method: 'post', 'data-type': 'html' })
$('form').append('<input type="text" name="content_type" value="application/xml">')
$('form').append('<input type="text" name="content" value="<p>hello</p>">')
submit(function(e, data, status, xhr) {
assert.ok(data instanceof Document, 'returned data should be an XML document')
done()
})
})
QUnit.test('accept application/json if "data-type" is json', function(assert) {
const done = assert.async()
buildForm({ method: 'post', 'data-type': 'json' })
submit(function(e, data, status, xhr) {
assert.equal(data.HTTP_ACCEPT, 'application/json, text/javascript, */*; q=0.01')
done()
})
})
QUnit.test('allow empty "data-remote" attribute', function(assert) {
const done = assert.async()
var form = $('#qunit-fixture').append($('<form action="/echo" data-remote />')).find('form')
submit(function() {
assert.ok(true, 'form with empty "data-remote" attribute is also allowed')
done()
})
})
QUnit.test('query string in form action should be stripped in a GET request in normal submit', function(assert) {
const done = assert.async()
buildForm({ action: '/echo?param1=abc', 'data-remote': 'false' })
$(document).one('iframe:loaded', function(e, data) {
assert.equal(data.params.param1, undefined, '"param1" should not be passed to server')
done()
})
$('#qunit-fixture form').triggerNative('submit')
})
QUnit.test('query string in form action should be stripped in a GET request in ajax submit', function(assert) {
const done = assert.async()
buildForm({ action: '/echo?param1=abc' })
submit(function(e, data, status, xhr) {
assert.equal(data.params.param1, undefined, '"param1" should not be passed to server')
done()
})
})
QUnit.test('query string in form action should not be stripped in a POST request in normal submit', function(assert) {
const done = assert.async()
buildForm({ action: '/echo?param1=abc', method: 'post', 'data-remote': 'false' })
$(document).one('iframe:loaded', function(e, data) {
assert.equal(data.params.param1, 'abc', '"param1" should be passed to server')
done()
})
$('#qunit-fixture form').triggerNative('submit')
})
QUnit.test('query string in form action should not be stripped in a POST request in ajax submit', function(assert) {
const done = assert.async()
buildForm({ action: '/echo?param1=abc', method: 'post' })
submit(function(e, data, status, xhr) {
assert.equal(data.params.param1, 'abc', '"param1" should be passed to server')
done()
})
})
QUnit.test('allow empty form "action"', function(assert) {
const done = assert.async()
var currentLocation, ajaxLocation
buildForm({ action: '' })
$('#qunit-fixture').find('form')
.bindNative('ajax:beforeSend', function(evt, xhr, settings) {
// Get current location (the same way jQuery does)
try {
currentLocation = location.href
} catch(err) {
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 https://bugs.jquery.com/ticket/10202 and https://github.com/jquery/jquery/pull/544)
ajaxLocation = settings.url.replace('user_name=john', '').replace(/&$/, '').replace(/\?$/, '')
assert.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.
evt.preventDefault()
})
.triggerNative('submit')
setTimeout(function() { done() }, 13)
})
QUnit.test('sends CSRF token in custom header', function(assert) {
const done = assert.async()
buildForm({ method: 'post' })
$('#qunit-fixture').append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae" />')
submit(function(e, data, status, xhr) {
assert.equal(data.HTTP_X_CSRF_TOKEN, 'cf50faa3fe97702ca1ae', 'X-CSRF-Token header should be sent')
done()
})
})
QUnit.test('intelligently guesses crossDomain behavior when target URL has a different protocol and/or hostname', function(assert) {
const done = assert.async()
// 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('<meta name="csrf-token" content="cf50faa3fe97702ca1ae" />')
$('#qunit-fixture').find('form')
.bindNative('ajax:beforeSend', function(evt, req, settings) {
assert.equal(settings.crossDomain, true, 'crossDomain should be set to true')
// prevent request from actually getting sent off-domain
evt.preventDefault()
})
.triggerNative('submit')
setTimeout(function() { done() }, 13)
})
QUnit.test('intelligently guesses crossDomain behavior when target URL consists of only a path', function(assert) {
const done = assert.async()
// 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('<meta name="csrf-token" content="cf50faa3fe97702ca1ae" />')
$('#qunit-fixture').find('form')
.bindNative('ajax:beforeSend', function(evt, req, settings) {
assert.equal(settings.crossDomain, false, 'crossDomain should be set to false')
// prevent request from actually getting sent off-domain
evt.preventDefault()
})
.triggerNative('submit')
setTimeout(function() { done() }, 13)
})

@ -1,21 +0,0 @@
import $ from 'jquery'
QUnit.module('csrf-refresh', {})
QUnit.test('refresh all csrf tokens', function(assert) {
var correctToken = 'cf50faa3fe97702ca1ae'
var form = $('<form />')
var input = $('<input>').attr({ type: 'hidden', name: 'authenticity_token', id: 'authenticity_token', value: 'foo' })
input.appendTo(form)
$('#qunit-fixture')
.append('<meta name="csrf-param" content="authenticity_token"/>')
.append('<meta name="csrf-token" content="' + correctToken + '"/>')
.append(form)
$.rails.refreshCSRFTokens()
var currentToken = $('#qunit-fixture #authenticity_token').val()
assert.equal(currentToken, correctToken)
})

@ -1,23 +0,0 @@
import $ from 'jquery'
QUnit.module('csrf-token', {})
QUnit.test('find csrf token', function(assert) {
var correctToken = 'cf50faa3fe97702ca1ae'
$('#qunit-fixture').append('<meta name="csrf-token" content="' + correctToken + '"/>')
var currentToken = $.rails.csrfToken()
assert.equal(currentToken, correctToken)
})
QUnit.test('find csrf param', function(assert) {
var correctParam = 'authenticity_token'
$('#qunit-fixture').append('<meta name="csrf-param" content="' + correctParam + '"/>')
var currentParam = $.rails.csrfParam()
assert.equal(currentParam, correctParam)
})

@ -1,372 +0,0 @@
import $ from 'jquery'
QUnit.module('data-confirm', {
beforeEach: function() {
$('#qunit-fixture').append($('<a />', {
href: '/echo',
'data-remote': 'true',
'data-confirm': 'Are you absolutely sure?',
text: 'my social security number'
}))
$('#qunit-fixture').append($('<button />', {
'data-url': '/echo',
'data-remote': 'true',
'data-confirm': 'Are you absolutely sure?',
text: 'Click me'
}))
$('#qunit-fixture').append($('<form />', {
id: 'confirm',
action: '/echo',
'data-remote': 'true'
}))
$('#qunit-fixture').append($('<input />', {
type: 'submit',
form: 'confirm',
'data-confirm': 'Are you absolutely sure?'
}))
$('#qunit-fixture').append($('<button />', {
type: 'submit',
form: 'confirm',
disabled: 'disabled',
'data-confirm': 'Are you absolutely sure?'
}))
this.windowConfirm = window.confirm
},
afterEach: function() {
window.confirm = this.windowConfirm
}
})
QUnit.test('clicking on a link with data-confirm attribute. Confirm yes.', function(assert) {
const done = assert.async()
var message
// auto-confirm:
window.confirm = function(msg) { message = msg; return true }
$('a[data-confirm]')
.bindNative('confirm:complete', function(e, data) {
assert.callbackInvoked('confirm:complete')
assert.ok(data == true, 'confirm:complete passes in confirm answer (true)')
})
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.callbackInvoked('ajax:success')
assert.requestPath(data, '/echo')
assert.getRequest(data)
assert.equal(message, 'Are you absolutely sure?')
done()
})
.triggerNative('click')
})
QUnit.test('clicking on a button with data-confirm attribute. Confirm yes.', function(assert) {
const done = assert.async()
var message
// auto-confirm:
window.confirm = function(msg) { message = msg; return true }
$('button[data-confirm]')
.bindNative('confirm:complete', function(e, data) {
assert.callbackInvoked('confirm:complete')
assert.ok(data == true, 'confirm:complete passes in confirm answer (true)')
})
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.callbackInvoked('ajax:success')
assert.requestPath(data, '/echo')
assert.getRequest(data)
assert.equal(message, 'Are you absolutely sure?')
done()
})
.triggerNative('click')
})
QUnit.test('clicking on a link with data-confirm attribute. Confirm No.', function(assert) {
const done = assert.async()
var message
// auto-decline:
window.confirm = function(msg) { message = msg; return false }
$('a[data-confirm]')
.bindNative('confirm:complete', function(e, data) {
assert.callbackInvoked('confirm:complete')
assert.ok(data == false, 'confirm:complete passes in confirm answer (false)')
})
.bindNative('ajax:beforeSend', function(e, data, status, xhr) {
assert.callbackNotInvoked('ajax:beforeSend')
})
.triggerNative('click')
setTimeout(function() {
assert.equal(message, 'Are you absolutely sure?')
done()
}, 50)
})
QUnit.test('clicking on a button with data-confirm attribute. Confirm No.', function(assert) {
const done = assert.async()
var message
// auto-decline:
window.confirm = function(msg) { message = msg; return false }
$('button[data-confirm]')
.bindNative('confirm:complete', function(e, data) {
assert.callbackInvoked('confirm:complete')
assert.ok(data == false, 'confirm:complete passes in confirm answer (false)')
})
.bindNative('ajax:beforeSend', function(e, data, status, xhr) {
assert.callbackNotInvoked('ajax:beforeSend')
})
.triggerNative('click')
setTimeout(function() {
assert.equal(message, 'Are you absolutely sure?')
done()
}, 50)
})
QUnit.test('clicking on a button with data-confirm attribute. Confirm error.', function(assert) {
const done = assert.async()
var message
// auto-decline:
window.confirm = function(msg) { message = msg; throw 'some random error' }
$('button[data-confirm]')
.bindNative('confirm:complete', function(e, data) {
assert.callbackInvoked('confirm:complete')
assert.ok(data == false, 'confirm:complete passes in confirm answer (false)')
})
.bindNative('ajax:beforeSend', function(e, data, status, xhr) {
assert.callbackNotInvoked('ajax:beforeSend')
})
.triggerNative('click')
setTimeout(function() {
assert.equal(message, 'Are you absolutely sure?')
done()
}, 50)
})
QUnit.test('clicking on a submit button with form and data-confirm attributes. Confirm No.', function(assert) {
const done = assert.async()
var message
// auto-decline:
window.confirm = function(msg) { message = msg; return false }
$('#qunit-fixture input[type=submit][form]')
.bindNative('confirm:complete', function(e, data) {
assert.callbackInvoked('confirm:complete')
assert.ok(data == false, 'confirm:complete passes in confirm answer (false)')
})
.bindNative('ajax:beforeSend', function(e, data, status, xhr) {
assert.callbackNotInvoked('ajax:beforeSend')
})
.triggerNative('click')
setTimeout(function() {
assert.equal(message, 'Are you absolutely sure?')
done()
}, 50)
})
QUnit.test('binding to confirm event of a link and returning false', function(assert) {
const done = assert.async()
// redefine confirm function so we can make sure it's not called
window.confirm = function(msg) {
assert.ok(false, 'confirm dialog should not be called')
}
$('a[data-confirm]')
.bindNative('confirm', function(e) {
assert.callbackInvoked('confirm')
e.preventDefault()
})
.bindNative('confirm:complete', function() {
assert.callbackNotInvoked('confirm:complete')
})
.triggerNative('click')
setTimeout(function() {
done()
}, 50)
})
QUnit.test('binding to confirm event of a button and returning false', function(assert) {
const done = assert.async()
// redefine confirm function so we can make sure it's not called
window.confirm = function(msg) {
assert.ok(false, 'confirm dialog should not be called')
}
$('button[data-confirm]')
.bindNative('confirm', function(e) {
assert.callbackInvoked('confirm')
e.preventDefault()
})
.bindNative('confirm:complete', function() {
assert.callbackNotInvoked('confirm:complete')
})
.triggerNative('click')
setTimeout(function() {
done()
}, 50)
})
QUnit.test('binding to confirm:complete event of a link and returning false', function(assert) {
const done = assert.async()
// auto-confirm:
window.confirm = function(msg) {
assert.ok(true, 'confirm dialog should be called')
return true
}
$('a[data-confirm]')
.bindNative('confirm:complete', function(e) {
assert.callbackInvoked('confirm:complete')
e.preventDefault()
})
.bindNative('ajax:beforeSend', function() {
assert.callbackNotInvoked('ajax:beforeSend')
})
.triggerNative('click')
setTimeout(function() {
done()
}, 50)
})
QUnit.test('binding to confirm:complete event of a button and returning false', function(assert) {
const done = assert.async()
// auto-confirm:
window.confirm = function(msg) {
assert.ok(true, 'confirm dialog should be called')
return true
}
$('button[data-confirm]')
.bindNative('confirm:complete', function(e) {
assert.callbackInvoked('confirm:complete')
e.preventDefault()
})
.bindNative('ajax:beforeSend', function() {
assert.callbackNotInvoked('ajax:beforeSend')
})
.triggerNative('click')
setTimeout(function() {
done()
}, 50)
})
QUnit.test('a button inside a form only confirms once', function(assert) {
const done = assert.async()
var confirmations = 0
window.confirm = function(msg) {
confirmations++
return true
}
$('#qunit-fixture').append($('<form />').append($('<button />', {
'data-remote': 'true',
'data-confirm': 'Are you absolutely sure?',
text: 'Click me'
})))
$('#qunit-fixture form > button[data-confirm]').triggerNative('click')
assert.ok(confirmations === 1, 'confirmation counter should be 1, but it was ' + confirmations)
done()
})
QUnit.test('clicking on the children of a link should also trigger a confirm', function(assert) {
const done = assert.async()
var message
// auto-confirm:
window.confirm = function(msg) { message = msg; return true }
$('a[data-confirm]')
.html('<strong>Click me</strong>')
.bindNative('confirm:complete', function(e, data) {
assert.callbackInvoked('confirm:complete')
assert.ok(data == true, 'confirm:complete passes in confirm answer (true)')
})
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.callbackInvoked('ajax:success')
assert.requestPath(data, '/echo')
assert.getRequest(data)
assert.equal(message, 'Are you absolutely sure?')
done()
})
.find('strong')
.triggerNative('click')
})
QUnit.test('clicking on the children of a disabled button should not trigger a confirm.', function(assert) {
const done = assert.async()
var message
// auto-decline:
window.confirm = function(msg) { message = msg; return false }
$('button[data-confirm][disabled]')
.html('<strong>Click me</strong>')
.bindNative('confirm', function() {
assert.callbackNotInvoked('confirm')
})
.find('strong')
.bindNative('click', function() {
assert.callbackInvoked('click')
})
.triggerNative('click')
setTimeout(function() {
done()
}, 50)
})
QUnit.test('clicking on a link with data-confirm attribute with custom confirm handler. Confirm yes.', function(assert) {
const done = assert.async()
var message, element
// redefine confirm function so we can make sure it's not called
window.confirm = function(msg) {
assert.ok(false, 'confirm dialog should not be called')
}
// custom auto-confirm:
Rails.confirm = function(msg, elem) { message = msg; element = elem; return true }
$('a[data-confirm]')
.bindNative('confirm:complete', function(e, data) {
assert.callbackInvoked('confirm:complete')
assert.ok(data == true, 'confirm:complete passes in confirm answer (true)')
})
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.callbackInvoked('ajax:success')
assert.requestPath(data, '/echo')
assert.getRequest(data)
assert.equal(message, 'Are you absolutely sure?')
assert.equal(element, $('a[data-confirm]').get(0))
done()
})
.triggerNative('click')
})

@ -1,490 +0,0 @@
import $ from 'jquery'
QUnit.module('data-disable-with', {
beforeEach: function() {
$('#qunit-fixture').append($('<form />', {
action: '/echo',
'data-remote': 'true',
method: 'post'
}))
.find('form')
.append($('<input type="text" data-disable-with="processing ..." name="user_name" value="john" />'))
$('#qunit-fixture').append($('<form />', {
action: '/echo',
method: 'post',
id: 'not_remote'
}))
.find('form:last')
// WEEIRDD: the form won't submit to an iframe if the button is name="submit" (??!)
.append($('<input type="submit" data-disable-with="submitting ..." name="submit2" value="Submit" />'))
$('#qunit-fixture').append($('<a />', {
text: 'Click me',
href: '/echo',
'data-disable-with': 'clicking...'
}))
$('#qunit-fixture').append($('<input />', {
type: 'submit',
form: 'not_remote',
'data-disable-with': 'form attr submitting',
name: 'submit3',
value: 'Form Attr Submit'
}))
$('#qunit-fixture').append($('<button />', {
text: 'Click me',
'data-remote': true,
'data-url': '/echo',
'data-disable-with': 'clicking...'
}))
$('#qunit-fixture').append($('<div />', {
id: 'edit-div', 'contenteditable': 'true'
}))
},
afterEach: function() {
$(document).unbind('iframe:loaded')
}
})
QUnit.test('form input field with "data-disable-with" attribute', function(assert) {
const done = assert.async()
var form = $('form[data-remote]'), input = form.find('input[type=text]')
assert.enabledState(input, 'john')
form.bindNative('ajax:success', function(e, data) {
setTimeout(function() {
assert.enabledState(input, 'john')
assert.equal(data.params.user_name, 'john')
done()
}, 13)
})
form.triggerNative('submit')
assert.disabledState(input, 'processing ...')
})
QUnit.test('blank form input field with "data-disable-with" attribute', function(assert) {
const done = assert.async()
var form = $('form[data-remote]'), input = form.find('input[type=text]')
input.val('')
assert.enabledState(input, '')
form.bindNative('ajax:success', function(e, data) {
setTimeout(function() {
assert.enabledState(input, '')
assert.equal(data.params.user_name, '')
done()
}, 13)
})
form.triggerNative('submit')
assert.disabledState(input, 'processing ...')
})
QUnit.test('form button with "data-disable-with" attribute', function(assert) {
const done = assert.async()
var form = $('form[data-remote]'), button = $('<button data-disable-with="submitting ..." name="submit2">Submit</button>')
form.append(button)
assert.enabledState(button, 'Submit')
form.bindNative('ajax:success', function(e, data) {
setTimeout(function() {
assert.enabledState(button, 'Submit')
done()
}, 13)
})
form.triggerNative('submit')
assert.disabledState(button, 'submitting ...')
})
QUnit.test('a[data-remote][data-disable-with] within a form disables and re-enables', function(assert) {
const done = assert.async()
var form = $('#qunit-fixture form:not([data-remote])'),
link = $('<a data-remote="true" data-disable-with="clicking...">Click me</a>')
form.append(link)
assert.enabledState(link, 'Click me')
link
.bindNative('ajax:beforeSend', function() {
assert.disabledState(link, 'clicking...')
})
.bindNative('ajax:complete', function() {
setTimeout( function() {
assert.enabledState(link, 'Click me')
link.remove()
done()
}, 15)
})
.triggerNative('click')
})
QUnit.test('form input[type=submit][data-disable-with] disables', function(assert) {
const done = assert.async()
var form = $('#qunit-fixture form:not([data-remote])'), input = form.find('input[type=submit]')
assert.enabledState(input, 'Submit')
$(document).bind('iframe:loaded', function(e, data) {
setTimeout(function() {
assert.disabledState(input, 'submitting ...')
done()
}, 30)
})
form.triggerNative('submit')
setTimeout(function() {
assert.disabledState(input, 'submitting ...')
}, 30)
})
QUnit.test('form input[type=submit][data-disable-with] re-enables when `pageshow` event is triggered', function(assert) {
var form = $('#qunit-fixture form:not([data-remote])'), input = form.find('input[type=submit]')
assert.enabledState(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.disableElement(form[0])
assert.disabledState(input, 'submitting ...')
$(window).triggerNative('pageshow')
assert.enabledState(input, 'Submit')
})
QUnit.test('form[data-remote] input[type=submit][data-disable-with] is replaced in ajax callback', function(assert) {
const done = assert.async()
var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'),
origFormContents = form.html()
form.bindNative('ajax:success', function() {
form.html(origFormContents)
setTimeout(function() {
var input = form.find('input[type=submit]')
assert.enabledState(input, 'Submit')
done()
}, 30)
}).triggerNative('submit')
})
QUnit.test('form[data-remote] input[data-disable-with] is replaced with disabled field in ajax callback', function(assert) {
const done = assert.async()
var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'),
input = form.find('input[type=submit]'),
newDisabledInput = input.clone().attr('disabled', 'disabled')
form.bindNative('ajax:success', function() {
input.replaceWith(newDisabledInput)
setTimeout(function() {
assert.enabledState(newDisabledInput, 'Submit')
done()
}, 30)
}).triggerNative('submit')
})
QUnit.test('form input[type=submit][data-disable-with] using "form" attribute disables', function(assert) {
var form = $('#not_remote'), input = $('input[form=not_remote]')
assert.enabledState(input, 'Form Attr Submit')
const done = assert.async()
$(document).bind('iframe:loaded', function(e, data) {
setTimeout(function() {
assert.disabledState(input, 'form attr submitting')
done()
}, 30)
})
form.triggerNative('submit')
setTimeout(function() {
assert.disabledState(input, 'form attr submitting')
}, 30)
})
QUnit.test('form[data-remote] textarea[data-disable-with] attribute', function(assert) {
const done = assert.async()
var form = $('form[data-remote]'),
textarea = $('<textarea data-disable-with="processing ..." name="user_bio">born, lived, died.</textarea>').appendTo(form)
form.bindNative('ajax:success', function(e, data) {
setTimeout(function() {
assert.equal(data.params.user_bio, 'born, lived, died.')
done()
}, 13)
})
form.triggerNative('submit')
assert.disabledState(textarea, 'processing ...')
})
QUnit.test('a[data-disable-with] disables', function(assert) {
const done = assert.async()
var link = $('a[data-disable-with]')
assert.enabledState(link, 'Click me')
link.triggerNative('click')
assert.disabledState(link, 'clicking...')
done()
})
QUnit.test('a[data-disable-with] re-enables when `pageshow` event is triggered', function(assert) {
var link = $('a[data-disable-with]')
assert.enabledState(link, 'Click me')
link.triggerNative('click')
assert.disabledState(link, 'clicking...')
$(window).triggerNative('pageshow')
assert.enabledState(link, 'Click me')
})
QUnit.test('a[data-remote][data-disable-with] disables and re-enables', function(assert) {
const done = assert.async()
var link = $('a[data-disable-with]').attr('data-remote', true)
assert.enabledState(link, 'Click me')
link
.bindNative('ajax:beforeSend', function() {
assert.disabledState(link, 'clicking...')
})
.bindNative('ajax:complete', function() {
setTimeout( function() {
assert.enabledState(link, 'Click me')
done()
}, 15)
})
.triggerNative('click')
})
QUnit.test('a[data-remote][data-disable-with] re-enables when `ajax:before` event is cancelled', function(assert) {
const done = assert.async()
var link = $('a[data-disable-with]').attr('data-remote', true)
assert.enabledState(link, 'Click me')
link
.bindNative('ajax:before', function(e) {
assert.disabledState(link, 'clicking...')
e.preventDefault()
})
.triggerNative('click')
setTimeout(function() {
assert.enabledState(link, 'Click me')
done()
}, 30)
})
QUnit.test('a[data-remote][data-disable-with] re-enables when `ajax:beforeSend` event is cancelled', function(assert) {
const done = assert.async()
var link = $('a[data-disable-with]').attr('data-remote', true)
assert.enabledState(link, 'Click me')
link
.bindNative('ajax:beforeSend', function(e) {
assert.disabledState(link, 'clicking...')
e.preventDefault()
})
.triggerNative('click')
setTimeout(function() {
assert.enabledState(link, 'Click me')
done()
}, 30)
})
QUnit.test('a[data-remote][data-disable-with] re-enables when `ajax:error` event is triggered', function(assert) {
const done = assert.async()
var link = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error')
assert.enabledState(link, 'Click me')
link
.bindNative('ajax:beforeSend', function() {
assert.disabledState(link, 'clicking...')
})
.bindNative('ajax:complete', function() {
setTimeout(function() {
assert.enabledState(link, 'Click me')
done()
}, 30)
})
.triggerNative('click')
})
QUnit.test('form[data-remote] input|button|textarea[data-disable-with] does not disable when `ajax:beforeSend` event is cancelled', function(assert) {
var form = $('form[data-remote]'),
input = form.find('input:text'),
button = $('<button data-disable-with="submitting ..." name="submit2">Submit</button>').appendTo(form),
textarea = $('<textarea data-disable-with="processing ..." name="user_bio">born, lived, died.</textarea>').appendTo(form),
submit = $('<input type="submit" data-disable-with="submitting ..." name="submit2" value="Submit" />').appendTo(form)
form
.bindNative('ajax:beforeSend', function(e) {
e.preventDefault()
e.stopPropagation()
})
.triggerNative('submit')
assert.enabledState(input, 'john')
assert.enabledState(button, 'Submit')
assert.enabledState(textarea, 'born, lived, died.')
assert.enabledState(submit, 'Submit')
})
QUnit.test('ctrl-clicking on a link does not disable the link', function(assert) {
var link = $('a[data-disable-with]')
assert.enabledState(link, 'Click me')
link.triggerNative('click', { metaKey: true })
assert.enabledState(link, 'Click me')
link.triggerNative('click', { metaKey: true })
assert.enabledState(link, 'Click me')
})
QUnit.test('right/mouse-wheel-clicking on a link does not disable the link', function(assert) {
var link = $('a[data-disable-with]')
assert.enabledState(link, 'Click me')
link.triggerNative('click', { button: 1 })
assert.enabledState(link, 'Click me')
link.triggerNative('click', { button: 1 })
assert.enabledState(link, 'Click me')
link.triggerNative('click', { button: 2 })
assert.enabledState(link, 'Click me')
link.triggerNative('click', { button: 2 })
assert.enabledState(link, 'Click me')
})
QUnit.test('button[data-remote][data-disable-with] disables and re-enables', function(assert) {
const done = assert.async()
var button = $('button[data-remote][data-disable-with]')
assert.enabledState(button, 'Click me')
button
.bindNative('ajax:send', function() {
assert.disabledState(button, 'clicking...')
})
.bindNative('ajax:complete', function() {
setTimeout( function() {
assert.enabledState(button, 'Click me')
done()
}, 15)
})
.triggerNative('click')
})
QUnit.test('button[data-remote][data-disable-with] re-enables when `ajax:before` event is cancelled', function(assert) {
const done = assert.async()
var button = $('button[data-remote][data-disable-with]')
assert.enabledState(button, 'Click me')
button
.bindNative('ajax:before', function(e) {
assert.disabledState(button, 'clicking...')
e.preventDefault()
})
.triggerNative('click')
setTimeout(function() {
assert.enabledState(button, 'Click me')
done()
}, 30)
})
QUnit.test('button[data-remote][data-disable-with] re-enables when `ajax:beforeSend` event is cancelled', function(assert) {
const done = assert.async()
var button = $('button[data-remote][data-disable-with]')
assert.enabledState(button, 'Click me')
button
.bindNative('ajax:beforeSend', function(e) {
assert.disabledState(button, 'clicking...')
e.preventDefault()
})
.triggerNative('click')
setTimeout(function() {
assert.enabledState(button, 'Click me')
done()
}, 30)
})
QUnit.test('button[data-remote][data-disable-with] re-enables when `ajax:error` event is triggered', function(assert) {
const done = assert.async()
var button = $('a[data-disable-with]').attr('data-remote', true).attr('href', '/error')
assert.enabledState(button, 'Click me')
button
.bindNative('ajax:send', function() {
assert.disabledState(button, 'clicking...')
})
.bindNative('ajax:complete', function() {
setTimeout(function() {
assert.enabledState(button, 'Click me')
done()
}, 30)
})
.triggerNative('click')
})
QUnit.test('form button with "data-disable-with" attribute and contenteditable is not modified', function(assert) {
const done = assert.async()
var form = $('form[data-remote]'), button = $('<button data-disable-with="submitting ..." name="submit2">Submit</button>')
var contenteditable_div = $('#qunit-fixture').find('div')
form.append(button)
contenteditable_div.append(form)
assert.enabledState(button, 'Submit')
setTimeout(function() {
assert.enabledState(button, 'Submit')
done()
}, 13)
form.triggerNative('submit')
assert.enabledState(button, 'Submit')
})

@ -1,387 +0,0 @@
import $ from 'jquery'
QUnit.module('data-disable', {
beforeEach: function() {
$('#qunit-fixture').append($('<form />', {
action: '/echo',
'data-remote': 'true',
method: 'post'
}))
.find('form')
.append($('<input type="text" data-disable name="user_name" value="john" />'))
$('#qunit-fixture').append($('<form />', {
action: '/echo',
method: 'post'
}))
.find('form:last')
// WEEIRDD: the form won't submit to an iframe if the button is name="submit" (??!)
.append($('<input type="submit" data-disable name="submit2" value="Submit" />'))
$('#qunit-fixture').append($('<a />', {
text: 'Click me',
href: '/echo',
'data-disable': 'true'
}))
$('#qunit-fixture').append($('<button />', {
text: 'Click me',
'data-remote': true,
'data-url': '/echo',
'data-disable': 'true'
}))
},
afterEach: function() {
$(document).unbind('iframe:loaded')
}
})
QUnit.test('form input field with "data-disable" attribute', function(assert) {
const done = assert.async()
var form = $('form[data-remote]'), input = form.find('input[type=text]')
assert.enabledState(input, 'john')
form.bindNative('ajax:success', function(e, data) {
setTimeout(function() {
assert.enabledState(input, 'john')
assert.equal(data.params.user_name, 'john')
done()
}, 13)
})
form.triggerNative('submit')
assert.disabledState(input, 'john')
})
QUnit.test('form button with "data-disable" attribute', function(assert) {
const done = assert.async()
var form = $('form[data-remote]'), button = $('<button data-disable name="submit2">Submit</button>')
form.append(button)
assert.enabledState(button, 'Submit')
form.bindNative('ajax:success', function(e, data) {
setTimeout(function() {
assert.enabledState(button, 'Submit')
done()
}, 13)
})
form.triggerNative('submit')
assert.disabledState(button, 'Submit')
assert.equal(button.data('ujs:enable-with'), undefined)
})
QUnit.test('form input[type=submit][data-disable] disables', function(assert) {
const done = assert.async()
var form = $('#qunit-fixture form:not([data-remote])'), input = form.find('input[type=submit]')
assert.enabledState(input, 'Submit')
// WEEIRDD: attaching this handler makes the test work in IE7
$(document).bind('iframe:loading', function(e, f) {})
$(document).bind('iframe:loaded', function(e, data) {
setTimeout(function() {
assert.disabledState(input, 'Submit')
done()
}, 30)
})
form.triggerNative('submit')
setTimeout(function() {
assert.disabledState(input, 'Submit')
}, 30)
})
QUnit.test('form[data-remote] input[type=submit][data-disable] is replaced in ajax callback', function(assert) {
const done = assert.async()
var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'), origFormContents = form.html()
form.bindNative('ajax:success', function() {
form.html(origFormContents)
setTimeout(function() {
var input = form.find('input[type=submit]')
assert.enabledState(input, 'Submit')
done()
}, 30)
}).triggerNative('submit')
})
QUnit.test('form[data-remote] input[data-disable] is replaced with disabled field in ajax callback', function(assert) {
const done = assert.async()
var form = $('#qunit-fixture form:not([data-remote])').attr('data-remote', 'true'), input = form.find('input[type=submit]'),
newDisabledInput = input.clone().attr('disabled', 'disabled')
form.bindNative('ajax:success', function() {
input.replaceWith(newDisabledInput)
setTimeout(function() {
assert.enabledState(newDisabledInput, 'Submit')
done()
}, 30)
}).triggerNative('submit')
})
QUnit.test('form[data-remote] textarea[data-disable] attribute', function(assert) {
const done = assert.async()
var form = $('form[data-remote]'),
textarea = $('<textarea data-disable name="user_bio">born, lived, died.</textarea>').appendTo(form)
form.bindNative('ajax:success', function(e, data) {
setTimeout(function() {
assert.equal(data.params.user_bio, 'born, lived, died.')
done()
}, 13)
})
form.triggerNative('submit')
assert.disabledState(textarea, 'born, lived, died.')
})
QUnit.test('a[data-disable] disables', function(assert) {
var link = $('a[data-disable]')
assert.enabledState(link, 'Click me')
link.triggerNative('click')
assert.disabledState(link, 'Click me')
assert.equal(link.data('ujs:enable-with'), undefined)
})
QUnit.test('a[data-remote][data-disable] disables and re-enables', function(assert) {
const done = assert.async()
var link = $('a[data-disable]').attr('data-remote', true)
assert.enabledState(link, 'Click me')
link
.bindNative('ajax:send', function() {
assert.disabledState(link, 'Click me')
})
.bindNative('ajax:complete', function() {
setTimeout( function() {
assert.enabledState(link, 'Click me')
done()
}, 15)
})
.triggerNative('click')
})
QUnit.test('a[data-remote][data-disable] re-enables when `ajax:before` event is cancelled', function(assert) {
const done = assert.async()
var link = $('a[data-disable]').attr('data-remote', true)
assert.enabledState(link, 'Click me')
link
.bindNative('ajax:before', function(e) {
assert.disabledState(link, 'Click me')
e.preventDefault()
})
.triggerNative('click')
setTimeout(function() {
assert.enabledState(link, 'Click me')
done()
}, 30)
})
QUnit.test('a[data-remote][data-disable] re-enables when `ajax:beforeSend` event is cancelled', function(assert) {
const done = assert.async()
var link = $('a[data-disable]').attr('data-remote', true)
assert.enabledState(link, 'Click me')
link
.bindNative('ajax:beforeSend', function(e) {
assert.disabledState(link, 'Click me')
e.preventDefault()
})
.triggerNative('click')
setTimeout(function() {
assert.enabledState(link, 'Click me')
done()
}, 30)
})
QUnit.test('a[data-remote][data-disable] re-enables when `ajax:error` event is triggered', function(assert) {
const done = assert.async()
var link = $('a[data-disable]').attr('data-remote', true).attr('href', '/error')
assert.enabledState(link, 'Click me')
link
.bindNative('ajax:send', function() {
assert.disabledState(link, 'Click me')
})
.bindNative('ajax:complete', function() {
setTimeout(function() {
assert.enabledState(link, 'Click me')
done()
}, 30)
})
.triggerNative('click')
})
QUnit.test('form[data-remote] input|button|textarea[data-disable] does not disable when `ajax:beforeSend` event is cancelled', function(assert) {
var form = $('form[data-remote]'),
input = form.find('input:text'),
button = $('<button data-disable="submitting ..." name="submit2">Submit</button>').appendTo(form),
textarea = $('<textarea data-disable name="user_bio">born, lived, died.</textarea>').appendTo(form),
submit = $('<input type="submit" data-disable="submitting ..." name="submit2" value="Submit" />').appendTo(form)
form
.bindNative('ajax:beforeSend', function(e) {
e.preventDefault()
e.stopPropagation()
})
.triggerNative('submit')
assert.enabledState(input, 'john')
assert.enabledState(button, 'Submit')
assert.enabledState(textarea, 'born, lived, died.')
assert.enabledState(submit, 'Submit')
})
QUnit.test('ctrl-clicking on a link does not disables the link', function(assert) {
var link = $('a[data-disable]')
assert.enabledState(link, 'Click me')
link.triggerNative('click', { metaKey: true })
assert.enabledState(link, 'Click me')
link.triggerNative('click', { ctrlKey: true })
assert.enabledState(link, 'Click me')
})
QUnit.test('right/mouse-wheel-clicking on a link does not disable the link', function(assert) {
var link = $('a[data-disable]')
assert.enabledState(link, 'Click me')
link.triggerNative('click', { button: 1 })
assert.enabledState(link, 'Click me')
link.triggerNative('click', { button: 1 })
assert.enabledState(link, 'Click me')
link.triggerNative('click', { button: 2 })
assert.enabledState(link, 'Click me')
link.triggerNative('click', { button: 2 })
assert.enabledState(link, 'Click me')
})
QUnit.test('button[data-remote][data-disable] disables and re-enables', function(assert) {
const done = assert.async()
var button = $('button[data-remote][data-disable]')
assert.enabledState(button, 'Click me')
button
.bindNative('ajax:send', function() {
assert.disabledState(button, 'Click me')
})
.bindNative('ajax:complete', function() {
setTimeout( function() {
assert.enabledState(button, 'Click me')
done()
}, 15)
})
.triggerNative('click')
})
QUnit.test('button[data-remote][data-disable] re-enables when `ajax:before` event is cancelled', function(assert) {
const done = assert.async()
var button = $('button[data-remote][data-disable]')
assert.enabledState(button, 'Click me')
button
.bindNative('ajax:before', function(e) {
assert.disabledState(button, 'Click me')
e.preventDefault()
})
.triggerNative('click')
setTimeout(function() {
assert.enabledState(button, 'Click me')
done()
}, 30)
})
QUnit.test('button[data-remote][data-disable] re-enables when `ajax:beforeSend` event is cancelled', function(assert) {
const done = assert.async()
var button = $('button[data-remote][data-disable]')
assert.enabledState(button, 'Click me')
button
.bindNative('ajax:beforeSend', function(e) {
assert.disabledState(button, 'Click me')
e.preventDefault()
})
.triggerNative('click')
setTimeout(function() {
assert.enabledState(button, 'Click me')
done()
}, 30)
})
QUnit.test('button[data-remote][data-disable] re-enables when `ajax:error` event is triggered', function(assert) {
const done = assert.async()
var button = $('a[data-disable]').attr('data-remote', true).attr('href', '/error')
assert.enabledState(button, 'Click me')
button
.bindNative('ajax:send', function() {
assert.disabledState(button, 'Click me')
})
.bindNative('ajax:complete', function() {
setTimeout(function() {
assert.enabledState(button, 'Click me')
done()
}, 30)
})
.triggerNative('click')
})
QUnit.test('do not enable elements for XHR redirects', function(assert) {
const done = assert.async()
var link = $('a[data-disable]').attr('data-remote', true).attr('href', '/echo?with_xhr_redirect=true')
assert.enabledState(link, 'Click me')
link
.bindNative('ajax:send', function() {
assert.disabledState(link, 'Click me')
})
.triggerNative('click')
setTimeout(function() {
assert.disabledState(link, 'Click me')
done()
}, 30)
})

@ -1,108 +0,0 @@
import $ from 'jquery'
QUnit.module('data-method', {
beforeEach: function() {
$('#qunit-fixture').append($('<a />', {
href: '/echo', 'data-method': 'delete', text: 'destroy!'
}))
$('#qunit-fixture').append($('<div />', {
id: 'edit-div', 'contenteditable': 'true'
}))
},
afterEach: function() {
$(document).unbind('iframe:loaded')
}
})
function submit(fn, options) {
$(document).bind('iframe:loaded', function(e, data) {
fn(data)
})
$('#qunit-fixture').find('a')
.triggerNative('click')
}
QUnit.test('link with "data-method" set to "delete"', function(assert) {
const done = assert.async()
submit(function(data) {
assert.equal(data.REQUEST_METHOD, 'DELETE')
assert.strictEqual(data.params.authenticity_token, undefined)
assert.strictEqual(data.HTTP_X_CSRF_TOKEN, undefined)
done()
})
})
QUnit.test('click on the child of link with "data-method"', function(assert) {
const done = assert.async()
$(document).bind('iframe:loaded', function(e, data) {
assert.equal(data.REQUEST_METHOD, 'DELETE')
assert.strictEqual(data.params.authenticity_token, undefined)
assert.strictEqual(data.HTTP_X_CSRF_TOKEN, undefined)
done()
})
$('#qunit-fixture a').html('<strong>destroy!</strong>').find('strong').triggerNative('click')
})
QUnit.test('link with "data-method" and CSRF', function(assert) {
const done = assert.async()
$('#qunit-fixture')
.append('<meta name="csrf-param" content="authenticity_token"/>')
.append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae"/>')
submit(function(data) {
assert.equal(data.params.authenticity_token, 'cf50faa3fe97702ca1ae')
done()
})
})
QUnit.test('link "target" should be carried over to generated form', function(assert) {
const done = assert.async()
$('a[data-method]').attr('target', 'super-special-frame')
submit(function(data) {
assert.equal(data.params._target, 'super-special-frame')
done()
})
})
QUnit.test('link with "data-method" and cross origin', function(assert) {
var data = {}
$('#qunit-fixture')
.append('<meta name="csrf-param" content="authenticity_token"/>')
.append('<meta name="csrf-token" content="cf50faa3fe97702ca1ae"/>')
$(document).on('submit', 'form', function(e) {
$(e.currentTarget).serializeArray().map(function(item) {
data[item.name] = item.value
})
return false
})
var link = $('#qunit-fixture').find('a')
link.attr('href', 'http://www.alfajango.com')
link.triggerNative('click')
assert.notEqual(data.authenticity_token, 'cf50faa3fe97702ca1ae')
})
QUnit.test('do not interact with contenteditable elements', function(assert) {
var contenteditable_div = $('#qunit-fixture').find('div')
contenteditable_div.append('<a href="http://www.shouldnevershowindocument.com" data-method="delete">')
var link = $('#edit-div').find('a')
link.triggerNative('click')
var collection = document.getElementsByTagName('form')
for (const item of collection) {
assert.notEqual(item.action, 'http://www.shouldnevershowindocument.com/')
}
})

@ -1,601 +0,0 @@
import $ from 'jquery'
function buildSelect(attrs) {
attrs = $.extend({
'name': 'user_data', 'data-remote': 'true', 'data-url': '/echo', 'data-params': 'data1=value1'
}, attrs)
$('#qunit-fixture').append(
$('<select />', attrs)
.append($('<option />', {value: 'optionValue1', text: 'option1'}))
.append($('<option />', {value: 'optionValue2', text: 'option2'}))
)
}
QUnit.module('data-remote', {
beforeEach: function() {
$('#qunit-fixture')
.append($('<a />', {
href: '/echo',
'data-remote': 'true',
'data-params': 'data1=value1&data2=value2',
text: 'my address'
}))
.append($('<button />', {
'data-url': '/echo',
'data-remote': 'true',
'data-params': 'data1=value1&data2=value2',
text: 'my button'
}))
.append($('<form />', {
action: '/echo',
'data-remote': 'true',
method: 'post',
id: 'my-remote-form'
}))
.append($('<a />', {
href: '/echo',
'data-remote': 'true',
disabled: 'disabled',
text: 'Disabled link'
}))
.find('form').append($('<input type="text" name="user_name" value="john">'))
$('#qunit-fixture').append($('<div />', {
id: 'edit-div', 'contenteditable': 'true'
}))
}
})
QUnit.test('ctrl-clicking on a link does not fire ajaxyness', function(assert) {
const done = assert.async()
assert.expect(0)
var link = $('a[data-remote]')
// Ideally, we'd set up an iframe to intercept normal link clicks
// and add a test to make sure the iframe:loaded event is triggered.
// However, jquery doesn't actually cause a native `click` event and
// follow links using `trigger('click')`, it only fires bindings.
link
.removeAttr('data-params')
.bindNative('ajax:beforeSend', function() {
assert.ok(false, 'ajax should not be triggered')
})
link.triggerNative('click', { metaKey: true })
link.triggerNative('click', { ctrlKey: true })
setTimeout(function() {
done()
}, 13)
})
QUnit.test('right/mouse-wheel-clicking on a link does not fire ajaxyness', function(assert) {
const done = assert.async()
assert.expect(0)
var link = $('a[data-remote]')
// Ideally, we'd set up an iframe to intercept normal link clicks
// and add a test to make sure the iframe:loaded event is triggered.
// However, jquery doesn't actually cause a native `click` event and
// follow links using `trigger('click')`, it only fires bindings.
link
.removeAttr('data-params')
.bindNative('ajax:beforeSend', function() {
assert.ok(false, 'ajax should not be triggered')
})
link.triggerNative('click', { button: 1 })
link.triggerNative('click', { button: 2 })
setTimeout(function() {
done()
}, 13)
})
QUnit.test('clicking on a link via a non-mouse Event (such as from js) works', function(assert) {
var link = $('a[data-remote]')
const done = assert.async()
link
.removeAttr('data-params')
.bindNative('ajax:beforeSend', function() {
assert.ok(true, 'ajax should be triggered')
})
Rails.fire(link[0], 'click')
setTimeout(function() { done() }, 13)
})
QUnit.test('ctrl-clicking on a link still fires ajax for non-GET links and for links with "data-params"', function(assert) {
var link = $('a[data-remote]')
const done = assert.async()
link
.removeAttr('data-params')
.attr('data-method', 'POST')
.bindNative('ajax:beforeSend', function() {
assert.ok(true, 'ajax should be triggered')
})
.triggerNative('click', { metaKey: true })
link
.removeAttr('data-method')
.attr('data-params', 'name=steve')
.triggerNative('click', { metaKey: true })
setTimeout(function() { done() }, 13)
})
QUnit.test('clicking on a link with data-remote attribute', function(assert) {
const done = assert.async()
$('a[data-remote]')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.callbackInvoked('ajax:success')
assert.requestPath(data, '/echo')
assert.equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
assert.equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value')
assert.getRequest(data)
})
.bindNative('ajax:complete', function() { done() })
.triggerNative('click')
})
QUnit.test('clicking on a link with both query string in href and data-params', function(assert) {
const done = assert.async()
$('a[data-remote]')
.attr('href', '/echo?data3=value3')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.getRequest(data)
assert.equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
assert.equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value')
assert.equal(data.params.data3, 'value3', 'query string in URL should be passed to server with right value')
})
.bindNative('ajax:complete', function() { done() })
.triggerNative('click')
})
QUnit.test('clicking on a link with both query string in href and data-params with POST method', function(assert) {
const done = assert.async()
$('a[data-remote]')
.attr('href', '/echo?data3=value3')
.attr('data-method', 'post')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.postRequest(data)
assert.equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
assert.equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value')
assert.equal(data.params.data3, 'value3', 'query string in URL should be passed to server with right value')
})
.bindNative('ajax:complete', function() { done() })
.triggerNative('click')
})
QUnit.test('clicking on a link with disabled attribute', function(assert) {
const done = assert.async()
assert.expect(0)
$('#qunit-fixture a[disabled]')
.bindNative('ajax:before', function(e, data, status, xhr) {
assert.callbackNotInvoked('ajax:success')
})
.triggerNative('click')
setTimeout(function() {
done()
}, 13)
})
QUnit.test('clicking on a button with data-remote attribute', function(assert) {
const done = assert.async()
$('button[data-remote]')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.callbackInvoked('ajax:success')
assert.requestPath(data, '/echo')
assert.equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
assert.equal(data.params.data2, 'value2', 'ajax arguments should have key data2 with right value')
assert.getRequest(data)
})
.bindNative('ajax:complete', function() { done() })
.triggerNative('click')
})
QUnit.test('right/mouse-wheel-clicking on a button with data-remote attribute does not fire ajaxyness', function(assert) {
const done = assert.async()
assert.expect(0)
var button = $('button[data-remote]')
// Ideally, we'd set up an iframe to intercept normal link clicks
// and add a test to make sure the iframe:loaded event is triggered.
// However, jquery doesn't actually cause a native `click` event and
// follow links using `trigger('click')`, it only fires bindings.
button
.removeAttr('data-params')
.bindNative('ajax:beforeSend', function() {
assert.ok(false, 'ajax should not be triggered')
})
button.triggerNative('click', { button: 1 })
button.triggerNative('click', { button: 2 })
setTimeout(function() {
done()
}, 13)
})
QUnit.test('changing a select option with data-remote attribute', function(assert) {
const done = assert.async()
buildSelect()
$('select[data-remote]')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.callbackInvoked('ajax:success')
assert.requestPath(data, '/echo')
assert.equal(data.params.user_data, 'optionValue2', 'ajax arguments should have key term with right value')
assert.equal(data.params.data1, 'value1', 'ajax arguments should have key data1 with right value')
assert.getRequest(data)
})
.bindNative('ajax:complete', function() { done() })
.val('optionValue2')
.triggerNative('change')
})
QUnit.test('submitting form with data-remote attribute', function(assert) {
const done = assert.async()
$('form[data-remote]')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.callbackInvoked('ajax:success')
assert.requestPath(data, '/echo')
assert.equal(data.params.user_name, 'john', 'ajax arguments should have key user_name with right value')
assert.postRequest(data)
})
.bindNative('ajax:complete', function() { done() })
.triggerNative('submit')
})
QUnit.test('submitting form with data-remote attribute should include inputs in a fieldset only once', function(assert) {
const done = assert.async()
$('form[data-remote]')
.append('<fieldset><input name="items[]" value="Item" /></fieldset>')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.callbackInvoked('ajax:success')
assert.equal(data.params.items.length, 1, 'ajax arguments should only have the item once')
assert.postRequest(data)
})
.bindNative('ajax:complete', function() {
$('form[data-remote], fieldset').remove()
done()
})
.triggerNative('submit')
})
QUnit.test('submitting form with data-remote attribute submits input with matching [form] attribute', function(assert) {
const done = assert.async()
$('#qunit-fixture')
.append($('<input type="text" name="user_data" value="value1" form="my-remote-form">'))
.append($('<input type="text" name="user_email" value="from@example.com" disabled="disabled" form="my-remote-form">'))
$('form[data-remote]')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.callbackInvoked('ajax:success')
assert.requestPath(data, '/echo')
assert.equal(data.params.user_name, 'john', 'ajax arguments should have key user_name with right value')
assert.equal(data.params.user_data, 'value1', 'ajax arguments should have key user_data with right value')
assert.equal(data.params.user_email, undefined, 'ajax arguments should not have disabled field')
assert.postRequest(data)
})
.bindNative('ajax:complete', function() { done() })
.triggerNative('submit')
})
QUnit.test('submitting form with data-remote attribute by clicking button with matching [form] attribute', function(assert) {
const done = assert.async()
$('form[data-remote]')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.callbackInvoked('ajax:success')
assert.requestPath(data, '/echo')
assert.equal(data.params.user_name, 'john', 'ajax arguments should have key user_name with right value')
assert.equal(data.params.user_data, 'value2', 'ajax arguments should have key user_data with right value')
assert.postRequest(data)
})
.bindNative('ajax:complete', function() { done() })
$('<button />', {
type: 'submit',
name: 'user_data',
value: 'value1',
form: 'my-remote-form'
})
.appendTo($('#qunit-fixture'))
$('<button />', {
type: 'submit',
name: 'user_data',
value: 'value2',
form: 'my-remote-form'
})
.appendTo($('#qunit-fixture'))
.triggerNative('click')
})
QUnit.test('form\'s submit bindings in browsers that don\'t support submit bubbling', function(assert) {
const done = assert.async()
var form = $('form[data-remote]'), directBindingCalled = false
assert.ok(!directBindingCalled, 'nothing is called')
form
.append($('<input type="submit" />'))
.bindNative('submit', function(event) {
assert.ok(event.type == 'submit', 'submit event handlers are called with submit event')
assert.ok(true, 'binding handler is called')
directBindingCalled = true
})
.bindNative('ajax:beforeSend', function() {
assert.ok(true, 'form being submitted via ajax')
assert.ok(directBindingCalled, 'binding handler already called')
})
.bindNative('ajax:complete', function() {
done()
})
if(!$.support.submitBubbles) {
// Must indirectly submit form via click to trigger jQuery's manual submit bubbling in IE
form.find('input[type=submit]')
.triggerNative('click')
} else {
form.triggerNative('submit')
}
})
QUnit.test('returning false in form\'s submit bindings in non-submit-bubbling browsers', function(assert) {
const done = assert.async()
var form = $('form[data-remote]')
form
.append($('<input type="submit" />'))
.bindNative('submit', function(e) {
assert.ok(true, 'binding handler is called')
e.preventDefault()
e.stopPropagation()
})
.bindNative('ajax:beforeSend', function() {
assert.ok(false, 'form should not be submitted')
})
if (!$.support.submitBubbles) {
// Must indirectly submit form via click to trigger jQuery's manual submit bubbling in IE
form.find('input[type=submit]').triggerNative('click')
} else {
form.triggerNative('submit')
}
setTimeout(function() { done() }, 13)
})
QUnit.test('clicking on a link with falsy "data-remote" attribute does not fire ajaxyness', function(assert) {
const done = assert.async()
assert.expect(0)
$('a[data-remote]')
.attr('data-remote', 'false')
.bindNative('ajax:beforeSend', function() {
assert.ok(false, 'ajax should not be triggered')
})
.bindNative('click', function(e) {
e.preventDefault()
})
.triggerNative('click')
setTimeout(function() { done() }, 20)
})
QUnit.test('ctrl-clicking on a link with falsy "data-remote" attribute does not fire ajaxyness even if "data-params" present', function(assert) {
const done = assert.async()
var link = $('a[data-remote]')
assert.expect(0)
link
.removeAttr('data-params')
.attr('data-remote', 'false')
.attr('data-method', 'POST')
.bindNative('ajax:beforeSend', function() {
assert.ok(false, 'ajax should not be triggered')
})
.bindNative('click', function(e) {
e.preventDefault()
})
.triggerNative('click', { metaKey: true })
link
.removeAttr('data-method')
.attr('data-params', 'name=steve')
.triggerNative('click', { metaKey: true })
setTimeout(function() { done() }, 20)
})
QUnit.test('clicking on a button with falsy "data-remote" attribute', function(assert) {
const done = assert.async()
assert.expect(0)
$('button[data-remote]:first')
.attr('data-remote', 'false')
.bindNative('ajax:beforeSend', function() {
assert.ok(false, 'ajax should not be triggered')
})
.bindNative('click', function(e) {
e.preventDefault()
})
.triggerNative('click')
setTimeout(function() { done() }, 20)
})
QUnit.test('submitting a form with falsy "data-remote" attribute', function(assert) {
const done = assert.async()
assert.expect(0)
$('form[data-remote]:first')
.attr('data-remote', 'false')
.bindNative('ajax:beforeSend', function() {
assert.ok(false, 'ajax should not be triggered')
})
.bindNative('submit', function(e) {
e.preventDefault()
})
.triggerNative('submit')
setTimeout(function() { done() }, 20)
})
QUnit.test('changing a select option with falsy "data-remote" attribute', function(assert) {
const done = assert.async()
assert.expect(0)
buildSelect({'data-remote': 'false'})
$('select[data-remote=false]:first')
.bindNative('ajax:beforeSend', function() {
assert.ok(false, 'ajax should not be triggered')
})
.val('optionValue2')
.triggerNative('change')
setTimeout(function() { done() }, 20)
})
QUnit.test('form should be serialized correctly', function(assert) {
const done = assert.async()
$('#qunit-fixture form')
.append('<textarea name="textarea">textarea</textarea>')
.append('<input type="checkbox" name="checkbox[]" value="0" />')
.append('<input type="checkbox" checked="checked" name="checkbox[]" value="1" />')
.append('<input type="radio" checked="checked" name="radio" value="0" />')
.append('<input type="radio" name="radio" value="1" />')
.append('<select multiple="multiple" name="select[]">\
<option value="1" selected>1</option>\
<option value="2" selected>2</option>\
<option value="3">3</option>\
<option selected>4</option>\
</select>')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.equal(data.params.checkbox.length, 1)
assert.equal(data.params.checkbox[0], '1')
assert.equal(data.params.radio, '0')
assert.equal(data.params.select.length, 3)
assert.equal(data.params.select[2], '4')
assert.equal(data.params.textarea, 'textarea')
done()
})
.triggerNative('submit')
})
QUnit.test('form buttons should only be serialized when clicked', function(assert) {
const done = assert.async()
$('#qunit-fixture form')
.append('<input type="submit" name="submit1" value="submit1" />')
.append('<button name="submit2" value="submit2" />')
.append('<button name="submit3" value="submit3" />')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.equal(data.params.submit1, undefined)
assert.equal(data.params.submit2, 'submit2')
assert.equal(data.params.submit3, undefined)
assert.equal(data['rack.request.form_vars'], 'user_name=john&submit2=submit2')
done()
})
.find('[name=submit2]').triggerNative('click')
})
QUnit.test('changing a select option without "data-url" attribute still fires ajax request to current location', function(assert) {
const done = assert.async()
var currentLocation, ajaxLocation
buildSelect({'data-url': ''})
$('select[data-remote]')
.bindNative('ajax:beforeSend', function(e, xhr, settings) {
// Get current location (the same way jQuery does)
try {
currentLocation = location.href
} catch(err) {
currentLocation = document.createElement( 'a' )
currentLocation.href = ''
currentLocation = currentLocation.href
}
ajaxLocation = settings.url.replace(settings.data, '').replace(/&$/, '').replace(/\?$/, '')
assert.equal(ajaxLocation, currentLocation, 'URL should be current page by default')
e.preventDefault()
})
.val('optionValue2')
.triggerNative('change')
setTimeout(function() { done() }, 20)
})
QUnit.test('inputs inside disabled fieldset are not submitted on remote forms', function(assert) {
const done = assert.async()
$('#qunit-fixture form')
.append('<fieldset>\
<input name="description" value="A wise man" />\
</fieldset>')
.append('<fieldset disabled="disabled">\
<input name="age" />\
</fieldset>')
.bindNative('ajax:success', function(e, data, status, xhr) {
assert.equal(data.params.user_name, 'john')
assert.equal(data.params.description, 'A wise man')
assert.equal(data.params.age, undefined)
done()
})
.triggerNative('submit')
})
QUnit.test('clicking on a link with contenteditable attribute does not fire ajaxyness', function(assert) {
const done = assert.async()
assert.expect(0)
var contenteditable_div = $('#qunit-fixture').find('div')
var link = $('a[data-remote]')
contenteditable_div.append(link)
link
.bindNative('ajax:beforeSend', function() {
assert.ok(false, 'ajax should not be triggered')
})
.bindNative('click', function(e) {
e.preventDefault()
})
.triggerNative('click')
setTimeout(function() { done() }, 20)
})

@ -1,52 +0,0 @@
import $ from 'jquery'
var realHref
QUnit.module('override', {
beforeEach: function() {
realHref = $.rails.href
$('#qunit-fixture')
.append($('<a />', {
href: '/real/href', 'data-remote': 'true', 'data-method': 'delete', 'data-href': '/data/href'
}))
},
afterEach: function() {
$.rails.href = realHref
}
})
QUnit.test('the getter for an element\'s href is publicly accessible', function(assert) {
assert.ok($.rails.href)
})
QUnit.test('the getter for an element\'s href is overridable', function(assert) {
$.rails.href = function(element) { return $(element).data('href') }
$('#qunit-fixture a')
.bindNative('ajax:beforeSend', function(e, xhr, options) {
assert.equal('/data/href', options.url)
e.preventDefault()
})
.triggerNative('click')
})
QUnit.test('the getter for an element\'s href works normally if not overridden', function(assert) {
$('#qunit-fixture a')
.bindNative('ajax:beforeSend', function(e, xhr, options) {
assert.equal(location.protocol + '//' + location.host + '/real/href', options.url)
e.preventDefault()
})
.triggerNative('click')
})
QUnit.test('the event selector strings are overridable', function(assert) {
assert.ok($.rails.linkClickSelector.indexOf(', a[data-custom-remote-link]') != -1, 'linkClickSelector contains custom selector')
})
QUnit.test('including rails-ujs multiple times throws error', function(assert) {
const done = assert.async()
assert.throws(function() {
Rails.start()
}, 'appending rails.js again throws error')
setTimeout(function() { done() }, 50)
})

@ -1,130 +0,0 @@
import $ from 'jquery'
import Rails from '../../../../app/javascript/rails-ujs/index'
$.rails = Rails
var App = App || {}
var Turbolinks = Turbolinks || {}
window.Turbolinks = Turbolinks
window.jQuery = $
QUnit.assert.callbackInvoked = function(callbackName) {
this.ok(true, callbackName + ' callback should have been invoked')
}
QUnit.assert.callbackNotInvoked = function(callbackName) {
this.ok(false, callbackName + ' callback should not have been invoked')
}
QUnit.assert.getRequest = function(requestEnv) {
this.equal(requestEnv['REQUEST_METHOD'], 'GET', 'request type should be GET')
}
QUnit.assert.postRequest = function(requestEnv) {
this.equal(requestEnv['REQUEST_METHOD'], 'POST', 'request type should be POST')
}
QUnit.assert.requestPath = function(requestEnv, path) {
this.equal(requestEnv['PATH_INFO'], path, 'request should be sent to right URL')
}
App.getVal = function(el) {
return el.is('input,textarea,select') ? el.val() : el.text()
}
App.disabled = function(el) {
return el.is('input,textarea,select,button') ?
(el.is(':disabled') && $.rails.getData(el[0], 'ujs:disabled')) :
$.rails.getData(el[0], 'ujs:disabled')
}
QUnit.assert.enabledState = function(el, text) {
this.ok(!App.disabled(el), el.get(0).tagName + ' should not be disabled')
this.equal(App.getVal(el), text, el.get(0).tagName + ' text should be original value')
}
QUnit.assert.disabledState = function(el, text) {
this.ok(App.disabled(el), el.get(0).tagName + ' should be disabled')
this.equal(App.getVal(el), text, el.get(0).tagName + ' text should be disabled value')
}
// hijacks normal form submit; lets it submit to an iframe to prevent
// navigating away from the test suite
$(document).bind('submit', function(e) {
if (!e.isDefaultPrevented()) {
var form = $(e.target), action = form.attr('action'),
name = 'form-frame' + jQuery.guid++,
iframe = $('<iframe name="' + name + '" />'),
iframeInput = '<input name="iframe" value="true" type="hidden" />',
targetInput = '<input name="_target" value="' + (form.attr('target') || '') + '" type="hidden" />'
if (action && action.indexOf('iframe') < 0) {
if (action.indexOf('?') < 0) {
form.attr('action', action + '?iframe=true')
} else {
form.attr('action', action + '&iframe=true')
}
}
form.attr('target', name).append(iframeInput, targetInput)
$('#qunit-fixture').append(iframe)
$.event.trigger('iframe:loading', form)
}
})
var _MouseEvent = window.MouseEvent
try {
new _MouseEvent()
} catch (e) {
_MouseEvent = function(type, options) {
var evt = document.createEvent('MouseEvents')
evt.initMouseEvent(type, options.bubbles, options.cancelable, window, options.detail, 0, 0, 80, 20, options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, null)
return evt
}
}
$.fn.extend({
// trigger a native click event
triggerNative: function(type, options) {
var el = this[0],
event,
Evt = {
'click': _MouseEvent,
'change': Event,
'pageshow': PageTransitionEvent,
'submit': Event
}[type]
options = options || {}
options.bubbles = true
options.cancelable = true
event = new Evt(type, options)
el.dispatchEvent(event)
if (type === 'submit' && !event.defaultPrevented) {
el.submit()
}
return this
},
bindNative: function(event, handler) {
if (!handler) return this
var el = this[0]
el.addEventListener(event, function(e) {
var args = []
if (e.detail) {
args = e.detail.slice()
}
args.unshift(e)
return handler.apply(el, args)
}, false)
return this
}
})
Turbolinks.clearCache = function() {}
Turbolinks.visit = function() {}

@ -1,68 +0,0 @@
# frozen_string_literal: true
require "rack"
require "rails"
require "action_controller/railtie"
require "action_view/railtie"
require "json"
module UJS
class Server < Rails::Application
routes.append do
match "/echo" => "tests#echo", via: :all
get "/error" => proc { |env| [403, { "content-type" => "text/plain" }, []] }
end
config.enable_reloading = true
config.eager_load = false
config.secret_key_base = "59d7a4dbd349fa3838d79e330e39690fc22b931e7dc17d9162f03d633d526fbb92dfdb2dc9804c8be3e199631b9c1fbe43fc3e4fc75730b515851849c728d5c7"
config.paths["app/views"].unshift("#{Rails.root}/views")
config.public_file_server.enabled = true
config.logger = Logger.new(STDOUT)
config.log_level = :error
config.hosts << proc { true }
config.content_security_policy do |policy|
policy.default_src :self, :https
policy.font_src :self, :https, :data
policy.img_src :self, :https, :data
policy.object_src :none
policy.script_src :self, :https
policy.style_src :self, :https
end
config.content_security_policy_nonce_generator = ->(req) { SecureRandom.base64(16) }
end
end
class TestsController < ActionController::Base
def echo
data = { params: params.to_unsafe_h }.update(request.env)
if params[:content_type] && params[:content]
render plain: params[:content], content_type: params[:content_type]
elsif request.xhr?
if params[:with_xhr_redirect]
response.set_header("X-Xhr-Redirect", "http://example.com/")
render inline: %{Turbolinks.clearCache()\nTurbolinks.visit("http://example.com/", {"action":"replace"})}
else
render json: JSON.generate(data)
end
elsif params[:iframe]
payload = JSON.generate(data).gsub("<", "&lt;").gsub(">", "&gt;")
html = <<-HTML
<script nonce="#{request.content_security_policy_nonce}">
if (window.top && window.top !== window)
window.parent.jQuery.event.trigger('iframe:loaded', #{payload})
</script>
<p>You shouldn't be seeing this. <a href="#{request.env['HTTP_REFERER']}">Go back</a></p>
HTML
render html: html.html_safe
else
render plain: "ERROR: #{request.path} requested without ajax", status: 404
end
end
end
UJS::Server.initialize!

@ -1,11 +0,0 @@
// Must go before rails-ujs.
document.addEventListener('rails:attachBindings', function() {
// This is for test in override.js.
window.Rails.linkClickSelector += ', a[data-custom-remote-link]';
// Hijacks link click before ujs binds any handlers
// This is only used for ctrl-clicking test on remote links
window.Rails.delegate(document, '#qunit-fixture a', 'click', function(e) {
e.preventDefault();
});
});

@ -1,17 +0,0 @@
import "./attach-bindings"
import Rails from '../../../app/javascript/rails-ujs/index'
import "../public/test/settings"
import "../public/test/data-confirm"
import "../public/test/data-remote"
import "../public/test/data-disable"
import "../public/test/data-disable-with"
import "../public/test/call-remote"
import "../public/test/call-remote-callbacks"
import "../public/test/data-method"
import "../public/test/override"
import "../public/test/csrf-refresh"
import "../public/test/csrf-token"
import "../public/test/call-ajax"

@ -621,10 +621,6 @@ URL helper.
<%= user_url(@user, host: 'example.com') %>
```
NOTE: non-`GET` links require [rails-ujs](https://github.com/rails/rails/blob/main/actionview/app/assets/javascripts) or
[jQuery UJS](https://github.com/rails/jquery-ujs), and won't work in mailer templates.
They will result in normal `GET` requests.
### Adding Images in Action Mailer Views
Unlike controllers, the mailer instance doesn't have any context about the

@ -160,7 +160,7 @@ To generate the guides in HTML format, you will need to install the guides depen
```bash
# only install gems necessary for the guides. To undo run: bundle config --delete without
$ bundle install --without job cable storage ujs test db
$ bundle install --without job cable storage test db
$ cd guides/
$ bundle exec rake guides:generate:html GUIDES_LANGUAGE=it-IT
```
@ -289,8 +289,6 @@ Inspecting 1 file
1 file inspected, no offenses detected
```
For `rails-ujs` CoffeeScript and JavaScript files, you can run `npm run lint` in `actionview` folder.
#### Spell Checking
We run [codespell](https://github.com/codespell-project/codespell) with GitHub Actions to check spelling and

@ -3,8 +3,7 @@
"workspaces": [
"actioncable",
"actiontext",
"activestorage",
"actionview"
"activestorage"
],
"dependencies": {
"webpack": "^4.17.1"

@ -3,7 +3,6 @@
"private": true,
"dependencies": {
"@rails/actioncable": "file:../../../../actioncable",
"@rails/activestorage": "file:../../../../activestorage",
"@rails/ujs": "file:../../../../actionview"
"@rails/activestorage": "file:../../../../activestorage"
}
}

@ -16,8 +16,6 @@
railties
)
FRAMEWORK_NAMES = Hash.new { |h, k| k.split(/(?<=active|action)/).map(&:capitalize).join(" ") }
NPM_PACKAGES = { "actionview" => "ujs" }
NPM_PACKAGES.default_proc = -> (_, framework) { framework }
root = File.expand_path("..", __dir__)
version = File.read("#{root}/RAILS_VERSION").strip
@ -123,10 +121,8 @@
if /[a-z]/.match?(version)
npm_tag = " --tag pre"
else
remote_package_version = `npm view @rails/#{NPM_PACKAGES[framework]}@latest version`.chomp
local_major_version = version.split(".", 4)[0]
remote_major_version = remote_package_version.split(".", 4)[0]
npm_tag = remote_major_version <= local_major_version ? " --tag latest" : " --tag v#{local_major_version}"
npm_tag = " --tag v#{local_major_version}"
end
sh "npm publish#{npm_tag}#{npm_otp}"

@ -28,11 +28,6 @@
resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.4.tgz"
integrity sha512-KE/SxsDqNs3rrWwFHcRh15ZLVFrI0YoZtgAdIyIq9k5hUNmiWRXXThPomIxHuL20sLdgzbDFyvkUMna14bvtrw==
"@jridgewell/sourcemap-codec@^1.4.15":
version "1.4.15"
resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@rails/activestorage@>= 7.1.0-alpha":
version "7.1.0-beta1"
resolved "https://registry.npmjs.org/@rails/activestorage/-/activestorage-7.1.0-beta1.tgz"
@ -65,14 +60,6 @@
is-module "^1.0.0"
resolve "^1.19.0"
"@rollup/plugin-replace@^5.0.4":
version "5.0.4"
resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.4.tgz#fef548dc751d06747e8dca5b0e8e1fbf647ac7e1"
integrity sha512-E2hmRnlh09K8HGT0rOnnri9OTh+BILGr7NVJGB30S4E3cLRn3J0xjdiyOZ74adPs4NiAMgrjUMGAZNJDBgsdmQ==
dependencies:
"@rollup/pluginutils" "^5.0.1"
magic-string "^0.30.3"
"@rollup/pluginutils@^3.1.0":
version "3.1.0"
resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz"
@ -82,25 +69,11 @@
estree-walker "^1.0.1"
picomatch "^2.2.2"
"@rollup/pluginutils@^5.0.1":
version "5.0.5"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.5.tgz#bbb4c175e19ebfeeb8c132c2eea0ecb89941a66c"
integrity sha512-6aEYR910NyP73oHiJglti74iRyOwgFU4x3meH/H8OJx6Ry0j6cOVZ5X/wTvub7G7Ao6qaHBEaNsV3GLJkSsF+Q==
dependencies:
"@types/estree" "^1.0.0"
estree-walker "^2.0.2"
picomatch "^2.3.1"
"@types/estree@*", "@types/estree@0.0.39":
version "0.0.39"
resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz"
integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==
"@types/estree@^1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/json5@^0.0.29":
version "0.0.29"
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
@ -1727,7 +1700,7 @@ eslint-visitor-keys@^1.0.0:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz"
integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
eslint@^4.19.1, eslint@^4.3.0:
eslint@^4.3.0:
version "4.19.1"
resolved "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz"
integrity sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==
@ -1813,7 +1786,7 @@ estree-walker@^1.0.1:
resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz"
integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==
estree-walker@^2.0.1, estree-walker@^2.0.2:
estree-walker@^2.0.1:
version "2.0.2"
resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
@ -2816,11 +2789,6 @@ jest-worker@^26.2.1:
merge-stream "^2.0.0"
supports-color "^7.0.0"
jquery@^2.2.0:
version "2.2.4"
resolved "https://registry.npmjs.org/jquery/-/jquery-2.2.4.tgz"
integrity sha512-lBHj60ezci2u1v2FqnZIraShGgEXq35qCzMv4lITyHGppTnA13rwR0MgwyNJh9TnDs3aXUvd1xjAotfraMHX/Q==
js-tokens@^3.0.2:
version "3.0.2"
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz"
@ -3072,13 +3040,6 @@ magic-string@^0.25.7:
dependencies:
sourcemap-codec "^1.4.8"
magic-string@^0.30.3:
version "0.30.5"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
make-dir@^2.0.0:
version "2.1.0"
resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz"
@ -3607,7 +3568,7 @@ performance-now@^2.1.0:
resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz"
integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1:
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2:
version "2.3.1"
resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
@ -3982,7 +3943,7 @@ rollup-plugin-terser@^7.0.2:
serialize-javascript "^4.0.0"
terser "^5.0.0"
rollup@^2.35.1, rollup@^2.53.3:
rollup@^2.35.1:
version "2.79.1"
resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz"
integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==